二进制与编码工具:Base64、Hex、xxd、od 与字节调试

系统讲解字节级调试工具箱:十进制/十六进制/二进制数制、Hex 与 Base64 编码(原理/表格/URL-safe/应用)、文本编码(ASCII/UTF-8)与字节的关系、xxd/od/hexdump 查看器、文件与协议调试(magic bytes/HTTP 十六进制)、位运算工具与常见踩坑。

引言

日志里突然冒出一串 aGVsbG8=,二进制协议抓包回来全是 \x00\xff,xxd 的转储看不懂——字节级调试是每个工程师迟早要跨的门槛。本文把这块"工具箱"一次补齐:先建立数制直觉(为什么 0xFF 是 255、位运算长什么样),再拆透两种最常见的编码——Hex(十六进制)与 Base64(各自解决什么问题、表格怎么来的、URL-safe 变体),接着讲文本与字节的关系(ASCII/UTF-8 在字节层面到底什么关系),最后给 xxd/od/hexdump 的实用姿势、magic bytes 识文件、以及调试二进制协议/文件时的实战套路与踩坑。

前置:/unicode-encoding-guide/(字符编码的完整理论)、/serialization-formats-compare/(二进制序列化格式)。网络与文件的字节处理见 [[network]]、[[python]]。


目录


1. 数制直觉:为什么 0xFF = 255

字节是 8 个 bit,能表示 0–255。不同数制只是"换个记法":

十进制  255
十六进制 0xFF   (FF = 15×16 + 15)
二进制  0b11111111 (8 个 1)
八进制  0377    (3×64 + 7×8 + 7)

为什么工程里常用十六进制:1 个 hex 位 = 4 个 bit——一个字节刚好 2 个 hex 位(FF),把字节串直接可读化,还能对齐内存布局。

十六进制对应表(前 16):

0x0=0 0x1=1 0x2=2 0x3=3 0x4=4 0x5=5 0x6=6 0x7=7
0x8=8 0x9=9 0xA=10 0xB=11 0xC=12 0xD=13 0xE=14 0xF=15

常用魔法数:

0xFF = 255        掩码取低 8 位
0xFFFF = 65535    IPv4 端口上限(2^16)
0x7F = 127        ASCII 上限
0x80 = 128        UTF-8 多字节首字节标志
0xFF 0xD8         JPEG magic
0x50 0x4B         'PK' = ZIP

记忆:一个 hex 位 = 4 bit,一个字节 = 2 hex 位——hex 是"人可读的二进制"。


2. Hex 编码:字节的直接字面

Hex 编码把每个字节写成 2 个 hex 字符——1 字节 → 2 字符,无损、一一对应、不改变数据含义(只是表示法)。

# Python 里
import binascii
data = b"Hi!"
print(data.hex())            # '486921'
print(bytes.fromhex("486921"))  # b'Hi!'

# 'H'=0x48, 'i'=0x69, '!'=0x21

Hex 的用武之地:

- 协议调试:看原始字节流(抓包、日志中的 hex dump)
- 哈希/签名展示:sha256 就是 64 个 hex 字符
- 内存/文件布局:对齐查看结构体、二进制格式
- MAC 地址、UUID 的标准文本形态

典型展示:

echo -n "Hi!" | xxd          # 00000000: 4869 210a                    Hi!.
#                             偏移        hex 内容                      ASCII

注意:hex 字符串是原始数据的 2 倍长度,没有压缩也不适合放进 HTTP 头(太长);要"体积更小地塞进文本",用 Base64。

记忆:Hex = 字节的直译(1:2),调试与哈希展示首选;要"紧凑地塞进文本"再上 Base64。


3. Base64:把二进制塞进文本

Base64 解决的问题:很多通道只接受可打印 ASCII(JSON、URL、邮件、HTML)。二进制字节(含 \x00、控制符、非 ASCII)直接放会乱、会被截断、会触发转义——于是用 64 个安全字符重新表示。

原理:把每 3 个字节(24 bit)切成 4 组 6 bit,每组查表(64 个字符):

64 字符表(标准):
A-Z  a-z  0-9  +  /     →  共 64 个
填充:=  (补足到 4 的倍数)

"Hi!" (3 bytes)
  0x48 0x69 0x21  =  010010 000110 100100 100001
  → 18 6 36 33 → 查表 S G k h → "SGkh"
import base64
base64.b64encode(b"Hi!")    # b'SGkh'
base64.b64decode(b"SGkh")   # b'Hi!'

体积账:3 字节 → 4 字符(3/4 比例,含填充约 +33%)——比 hex(1:2)紧凑,但仍比原始大 33%。

典型应用:

- JWT:三段 Base64url 编码(header.payload.signature)
- 邮件附件(MIME):任意二进制转文本
- JSON 里嵌二进制/图片(data URI:data:image/png;base64,...)
- API 鉴权:Basic Auth(base64("user:pass"))
# data URI:小图片直接嵌进 HTML/JSON
import base64
png = open("icon.png", "rb").read()
uri = f"data:image/png;base64,{base64.b64encode(png).decode()}"

记忆:Base64 是"二进制的文本皮",3→4 膨胀 33%;它不加密也不压缩,只是换可打印字符。


4. URL-safe 与 Base64 的变体

标准 Base64 的 + 和 / 在 URL/文件名里不友好(+ 是空格、/ 是路径分隔),于是有 Base64url:

import base64
b64 = base64.urlsafe_b64encode(b"\xff\xee")   # 用 - 和 _ 代替 + 和 /
# b'_-4='

Base64 家族速查:

变体字符集填充用途
标准 Base64A-Za-z0-9+/=邮件、data URI
Base64urlA-Za-z0-9-_=(可去)JWT、URL 参数
Base32A-Z2-7=可读性/人工输入(TOTP)
Base16(Hex)0-9A-F无哈希、调试
Base58去 0OIl无比特币地址(去歧义)

Base32 的独特价值:去掉易混淆字符(0/O、1/I)后人类可读、可手输——这就是 TOTP(Google Authenticator)用 Base32 的原因。

去填充注意:JWT 常省略 =(无填充 Base64url)。解码时要能容忍"无填充":

import base64
def b64url_decode(s):
    s += '=' * (-len(s) % 4)          # 补回填充
    return base64.urlsafe_b64decode(s)

一个坑:base64.b64encode 的默认 validate=False 会静默忽略非法字符——调试"解码报错"时先看是不是填充/换行问题。

记忆:Base64url 换掉 +/ 为 -_,是 URL 与 JWT 的标准;Base32 求可读性,Base58 求无歧义——按通道选变体。


5. 文本与字节:ASCII、UTF-8 与乱码

字节≠字符——同一个字节序列在不同编码下读出不同文本,这就是乱码的根源。

ASCII(0x00–0x7F):128 个字符,1 字节 1 字符,高位恒为 0。

UTF-8(今天的事实标准):可变长,ASCII 与单字节兼容,中文 3 字节:

U+0000–007F   1 字节  0xxxxxxx         ASCII 原样
U+0080–07FF   2 字节  110xxxxx 10xxxxxx
U+0800–FFFF   3 字节  1110xxxx 10xxxxxx 10xxxxxx   ← 中文在此
U+10000+      4 字节  11110xxx ...
"中".encode("utf-8")   # b'\xe4\xb8\xad'(3 字节)

乱码的典型来源:

- 源文件是 GBK/GB2312,读取/解析按 UTF-8 → 全是锟斤拷/��
- 网络请求头 Content-Type 缺 charset → 浏览器猜编码
- 数据库连接未指定 utf8mb4 → 中文截断
- 终端 locale 错 → 显示乱码

字节层调试:看"到底是什么字节",用 xxd/od/Python:

printf '中文' | xxd          # e4 b8 ad e6 96 87 → 3+3 字节 UTF-8
printf '中文' | iconv -f UTF-8 -t GBK | xxd   # 转 GBK 后字节不同
s = "中文"
print([hex(b) for b in s.encode("utf-8")])   # ['0xe4','0xb8','0xad',...]

记忆:乱码是"同一字节不同解码"的误会——用 xxd 看字节、用 iconv 换编码、用 charset 定契约。理论完整版见 /unicode-encoding-guide/。


6. xxd、od 与 hexdump:字节查看器

三大命令行查看器的定位:

工具特点何时用
xxdVim 出品,支持 -r 反向日常查看、快速 hex
od最老牌,数制任意(octal/hex/char)可移植脚本、八进制
hexdump(hd)多格式布局、-C 经典文件结构分析

xxd 常用姿势:

xxd file.bin                     # hex + ASCII 对照
xxd -l 64 file.bin               # 只看前 64 字节
xxd -g 1 file.bin                # 每字节分组显示
xxd -r hex.txt > out.bin         # hex → 二进制(反向)

od 常用姿势:

od -A d -t x1 file.bin     # 十进制偏移 + 单字节 hex
od -c file.bin             # 按字符显示(含转义 \n \0)
od -t u2 file.bin          # 按 2 字节无符号整数读

hexdump -C 经典输出:

00000000  48 69 21 0a 00 ff   |Hi!...|

读转储的方法:左列是字节偏移(16 进制),中间是 hex 字节,右侧是 ASCII 可读区(不可打印显示 .)。

记忆:xxd 日常、od 可移植、hexdump -C 结构化——三个都会看偏移列与 ASCII 区,二进制就"读"得动了。


7. magic bytes 与文件类型识别

文件系统/浏览器靠扩展名,但真实识别靠文件头(magic bytes):

文件类型文件头(hex)ASCII
JPEGFF D8 FFÿØÿ
PNG89 50 4E 47‰PNG
GIF47 49 46 38GIF8
ZIP50 4B 03 04PK..
PDF25 50 44 46%PDF
gzip1F 8B.‹
ELF(可执行)7F 45 4C 46\x7fELF
xxd -l 8 photo.jpg         # 看文件头
file photo.jpg             # 用 libmagic 识别(读 magic 库)

Python 侧快速识别:

with open("unknown", "rb") as f:
    head = f.read(8)
    if head[:2] == b"\xff\xd8": print("JPEG")
    elif head[:4] == b"\x89PNG": print("PNG")
    elif head[:4] == b"PK\x03\x04": print("ZIP")

工程场景:文件上传校验"是否真是图片"不能只信扩展名,要读 magic;恶意文件常改扩展名骗过过滤器——识别走 magic、上传白名单走 content-type + magic 双校验(详见 [[security]] 的文件上传安全)。

记忆:文件类型看 magic bytes 不看扩展名;上传校验要 magic + content-type 双保险。


8. 位运算与打包解包

二进制格式里频繁出现位打包(一个字节存多个标志位)、掩码、位移:

flags = 0b1101_0000          # 一个字节存 8 个布尔
BIT_SET = 0b0000_0010

flags & BIT_SET              # 检查第 2 位 → 0(未设)
flags | BIT_SET              # 置位
flags & ~BIT_SET             # 清零
flags ^ BIT_SET              # 翻转
(flags >> 4) & 0x0F          # 取高 4 位 → 0b1101 = 13

网络协议常见:大端(big-endian)还是小端?TCP/IP 用大端,x86 内存是小端:

import struct
# struct 是打包/解包的瑞士军刀
struct.pack(">I", 0xDEADBEEF)   # 大端 4 字节
struct.pack("<I", 0xDEADBEEF)   # 小端 4 字节
struct.unpack(">HH", b"\x00\x01\x00\x02")  # (1, 2)
printf '\xde\xad\xbe\xef' | xxd   # 大端写入
# Python: int.from_bytes(b'\xde\xad\xbe\xef', 'big') == 0xDEADBEEF

位域/对齐:C 结构体有字节对齐(padding),读二进制结构体时要注意字段偏移——用 struct 的格式串显式控制,别靠"数着读"。

记忆:位运算管"一个字节塞多个标志",struct 管"跨端序打包解包"——协议调试先确认端序与对齐。


9. 实战:调试二进制协议与文件

场景:抓到的 TCP 报文全是十六进制

1. 确认端序(大端/小端)与字段宽度
2. 按协议文档切字段:用 xxd/od 或 Python struct
3. 标记 magic/长度前缀 → 检查长度字段对不对
4. 把可读部分(ASCII)单独提出来看 → 定位具体内容
# 示例:自定义协议 [4B 长度][2B 类型][payload]
import struct
buf = b"\x00\x00\x00\x04\x00\x01Hi\x00\x00"
length, kind = struct.unpack(">IH", buf[:6])   # 长度 4、类型 1
payload = buf[6:6+length]                       # b'Hi\x00\x00'

场景:日志里的一串 Base64 看不懂

echo 'eyJhbGciOiJIUzI1NiJ9' | base64 -d   # {"alg":"HS256"}
# JWT 三段:header.payload.signature,每段 base64url

场景:文件头对不上导致解析失败

先 xxd -l 16 看头部 → 对比 magic 表
常见:BOM(EF BB BF)没处理、行尾 \r\n 混入、压缩包被二次 gzip

调试铁律:

□ 永远从"原始字节"出发,别相信打印出来的"看起来像"
□ 一次只看一个字段:拆开、标注、再合起来
□ 用确定性样例(构造最小复现)而不是大海捞针

记忆:二进制调试 = 端序 + 字段切分 + magic 对照 + 最小复现——从字节出发,不信’打印’。


10. 速查表与一句话记忆

全篇速查:

主题结论
数制1 hex 位 = 4 bit,一字节 = 2 hex 位
Hex字节直译 1:2,调试/哈希展示
Base643→4,+33%,塞进文本通道
Base64url-_ 代替 +/,JWT/URL
Base32/58可读性/无歧义
乱码同一字节不同解码,用 iconv/xxd
xxd/od看偏移 + hex + ASCII 区
magic文件头识类型,上传双校验
位运算一个字节塞多标志
struct端序与打包解包
调试字节出发 + 字段切分 + 最小复现

一句话记忆:字节是一切的本源——hex 是直译、Base64 是"塞进文本的皮"、Base64url 服务 URL/JWT;乱码是解码契约的误会,用 xxd/iconv 看清字节;文件识别看 magic 不看扩展名;协议调试先定端序再切字段,用 struct 打包解包、位运算管标志——从字节出发,二进制就不再有玄学。


延伸阅读

  • /unicode-encoding-guide/ — 字符编码完整理论
  • /serialization-formats-compare/ — Protobuf/JSON 等序列化的字节布局
  • /regex-deep-dive/ — 字节流上的文本模式
  • [[security]] — 文件上传校验与 magic bytes
  • [[network]] — 网络协议与抓包分析

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「others」更多文章

  1. 通配符与 Glob 匹配:与正则的分野与落地
  2. 算法复杂度速查:Big-O、空间复杂度与工程直觉
  3. 正则表达式深层解析:引擎、回溯与灾难性回溯