第2章:以太网帧结构——前导码、定界符、MAC地址、类型/长度字段、数据与FCS校验
看网络数据,第一关就是认帧。很多人上来就抓包,盯着十六进制看半天,其实只要把帧结构掰开揉碎,读数据就简单多了。我个人习惯,先记住这六个字段怎么拼起来,后面几乎不会跑偏。
2.1 前导码与帧定界符——帧的开始
收发双方是靠电信号同步的。前导码(Preamble)就是干这个的。它一共7个字节,每字节都是 0xAA,也就是比特序列 10101010。说白了,就是让接收端把时钟锁住,为后面的数据做准备。
然后是1个字节的帧定界符(SFD,Start Frame Delimiter),值是 0xAB,也就是 10101011。最后两位变成了“11”,这就像发了个信号:“认真听,真正的帧来了”。
我们真的需要关心前导码吗?嗯,如果你只做应用层测试,确实不用碰它。但如果你在写FPGA的MAC层代码,或者在测PHY芯片的一致性,这段序列就是硬性门槛。我记得有一次做10BASE-T的抖动测试,发现前导码脉冲宽度超了,整个帧就被后端丢弃了。折腾了两天,才发现问题出在这里。
2.2 MAC地址——帧的收件人与发件人
MAC地址,全称Media Access Control address。它占6个字节,分两部分:目的MAC和源MAC。
目的MAC在前,源MAC在后。这个顺序是强制的,别记反了。为什么?早期的以太网设备是边收边转发,先看到目的地址就能更快决策。你想想看,这个设计挺聪明的。
| 地址类型 | 第一位比特(LSB方向) | 作用 |
|---|---|---|
| 单播 | 0 | 发给唯一目标 |
| 多播 | 1 | 发给一组设备 |
| 广播 | 全1(FF:FF:FF:FF:FF:FF) | 发给所有设备 |
这里有个很容易踩的坑:MAC地址的bit顺序。标准里规定,目的MAC的第一个字节的LSB(最低位)用来区分单播/多播。但很多交换芯片在处理时,是从LSB向MSB看的。我曾经在写一致性测试用例时,故意把目的MAC的多播位设反,结果交换机直接当单播处理了。嗯,这就是硬件和规范的偏差。
2.3 类型/长度字段——二选一的复用
紧跟着源MAC的,是2个字节的类型/长度字段。这是个复用字段。
- 长度模式:当值
≤ 1500(0x05DC),表示数据字段的实际长度。 - 类型模式:当值
≥ 1536(0x0600),表示上层协议类型。比如0x0800是IPv4,0x0806是ARP。
为什么要留一段“灰色地带”?因为1500到1536之间的值没有定义,用来做区分。你想想看,这两套用法不能混,协议栈拿到帧以后,第一步就是判断这个值是长度还是类型。
| 字段值范围 | 含义 | 协议栈处理 | 示例 |
|---|---|---|---|
| 0x0000 ~ 0x05DC | 数据长度 | 从帧中提取数据并按LLC处理 | 0x002E(46字节数据) |
| 0x0600 ~ 0xFFFF | EtherType | 直接交付给对应协议模块 | 0x0800→IPv4 |
我个人习惯在看抓包结果时,先扫一眼这个字段。如果显示 Type: IPv4 (0x0800),那说明这条帧用的是类型模式。如果显示 Length: 64,那就是长度模式。现在的网络几乎全是类型模式,长度模式多见于老式的802.3网络。
2.4 数据字段——实际负担
数据字段(Payload)最短46字节,最长1500字节。为什么有下限?因为MAC地址、类型、FCS加起来是18字节,帧最短是64字节(不包括前导码和SFD),所以数据至少补到46字节。如果实际数据不够,就填充(Pad)为零。
为什么帧不能低于64字节?你想想看,如果帧太短,收发器还没来得及检测碰撞,帧就发完了。CSMA/CD要求帧长度 >= 碰撞槽时间(512比特时间),所以限制64字节。
上限1500字节,是为了控制一个设备的缓冲区大小,同时也避免一个错误帧占用太久的链路。现在巨帧(Jumbo Frame)可以到9000字节,但那不是标准以太网,只在数据中心内部约定用。
2.5 FCS校验——最后一道防线
FCS(Frame Check Sequence)占4字节,用的是CRC-32算法。计算范围从目的MAC开始,到数据字段结束,不包括前导码和SFD。生成多项式是:
0x04C11DB7
我把它写在代码里,你感受一下:
// 标准CRC-32计算片段
uint32_t crc32_eth(const uint8_t *data, int len) {
uint32_t crc = 0xFFFFFFFF;
for (int i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc >>= 1;
}
}
return ~crc;
}
接收端算一遍CRC,如果不等于0,直接丢帧。为什么?因为CRC的错误检测能力非常强。我遇到过一种特别隐蔽的情况:链路噪声导致MAC地址中间一个bit翻转,但CRC没变,因为那个bit的翻转刚好被另一个比特的翻转抵消了。这叫“错误漏检”,概率极低,但存在。一般网卡对这种情况无能为力,只能依赖上层协议(TCP等)重传。
eth.fcs_bad 来抓出所有FCS错误的帧,快速定位问题。
也有人说,FCS校验是否有必要全程做?嗯,如果你是做交换机的线速转发,某些场景下可以只查目的MAC就转发,FCS转出去再补。但标准里明确要求,交换机必须校验FCS后才能转发,除非你开启“直通转发”(Cut-Through)。不过,直通模式发出去的帧如果本身有错,下游设备还是会丢掉。
2.6 帧结构全貌
把上一节的内容串起来,以太网帧就是这样拼装的。我用一张图帮你做整体回顾:
收到一个以太网帧之后,到底怎么知道它没坏?FCS就是干这个的。接收端按同样的CRC算法再算一遍,结果应该是 0xDEBB20E3(即剩余余数为0的魔术数叫法),实际比较时直接看算出来的CRC跟帧尾的4字节是否一致就行了。
这就是以太网帧的完整结构。每一段都有明确的分工,从同步(前导码)、定界(SFD)、寻址(MAC)、协议标识(类型/长度)、数据传输(数据)到完整性校验(FCS),一个环节都不能出错。做一致性测试,其实就是拿帧的每个字段去比对标准,哪里不符哪里就是测试失败。