Skip to content

第三章:编译器秘法

涉及内核源码: ,

楔子

林小源理解了指针和结构体,但他很快发现了新的问题。

他看到一段代码,里面写着 typeof(x)——这不是一个函数,也不是一个变量,它在编译时就被替换了。他看到另一段代码,__attribute__((aligned(64)))——这像是某种指令,但它不在汇编里,而是在 C 代码中。

这些是什么?

他开始观察编译器的行为。他发现,编译器不仅仅是把 C 代码翻译成机器指令——它会重排代码的顺序,会内联小函数,会删除永远不会执行的分支。

更让他惊讶的是,编译器有一些"秘密指令"——GCC 扩展。这些扩展不是标准 C 的一部分,但内核大量使用它们。

获取表达式的类型。 控制变量和函数的属性。 系列直接映射到硬件指令。

这些是编译器的秘法。

掌握它们,你才能读懂内核中那些"看起来不像 C"的代码。

一、aligned 与 packed — 松紧自如

林小源第一次见到编译器,是在一座古老的大殿里。

大殿的主人是一个老者,身披灰袍,手中握着一把巨大的锤子——编译器的符号。他的眼睛深不可测,仿佛能看穿一切代码的本质。

"你来了。"老者的声音回荡在大殿中,"你想读懂内核的代码?那得先学会我的语言。"

林小源环顾四周。大殿的地板上刻着各种属性声明————像是某种古老的符文。

老者指向地板上两块相邻的区域。左边的区域紧凑得像一块压缩饼干,右边的区域宽敞得像一座宫殿。

"同样的两个字段——char aint b——用 ,它们紧紧贴在一起,5 字节。用 aligned(64),它们被拉伸到 64 字节的边界上。"老者敲了敲地板,"packed 用于网络协议头、硬件寄存器这些必须精确控制布局的场景。aligned 用于缓存行对齐——避免多核之间的 false sharing。"

"false sharing?"

"两个 CPU 核心各自修改不同的变量,但这两个变量恰好在同一个缓存行里。结果就是——每次修改都要通知对方的缓存失效,白白浪费时间。用 把热数据对齐到缓存行边界,就能避免这个问题。"

林小源蹲下来仔细看那两块区域。紧凑的那一块严丝合缝,没有一丝浪费;宽敞的那一块虽然浪费了空间,但每一块数据都恰好落在它该在的位置上。

"松紧之间,各有各的道理。"老者说。

二、typeof — 类型的反射

老者带着林小源来到一面镜子前。镜子不是反射光线的——它反射的是类型

"这是 ,"老者说,"GCC 扩展。它在编译时获取表达式的类型,但不求值。"

林小源把一个 int x 举到镜子前。镜子里映出了 int。他把一个 double d 举过去,镜子里映出了

"有什么用?"

老者从袖中掏出一张纸条,上面写着一个宏:

c
#define swap(a, b) do { \
    typeof(a) __tmp = (a); \
    (a) = (b); \
    (b) = __tmp; \
} while (0)

"没有 ,你写 宏就得用固定的类型——比如 int。但有了 可以用于任何类型。int——都行。"

林小源接过纸条,翻到背面。那里写着另一个宏:

c
#define min(x, y) ({                    \
    typeof(x) _min1 = (x);              \
    typeof(y) _min2 = (y);              \
    (void)(&_min1 == &_min2);           \
    _min1 < _min2 ? _min1 : _min2;      \
})

"等等,"林小源指着中间那行,"(&_min1 == &_min2) 是什么?比较两个指针?"

老者笑了:"那是类型安全检查。如果 xy 的类型不同,_min1_min2 的类型就不同,它们的指针比较会产生编译器警告。这个宏在编译期就阻止了类型不匹配——而不会在运行时给你一个莫名其妙的结果。"

三、语句表达式 — 宏的利器

老者又从袖中掏出另一张纸条。上面的宏更复杂了:

c
#define safe_max(a, b) ({      \
    typeof(a) _a = (a);        \
    typeof(b) _b = (b);        \
    _a > _b ? _a : _b;        \
})

"注意 ({...}) 的语法,"老者说,"这是 GCC 的语句表达式——在表达式中使用复合语句。最后一个表达式的值作为整个块的值。"

林小源试着理解。花括号里可以声明临时变量、执行多条语句,最后返回一个值——像函数一样安全,但没有函数调用的开销。

"内核中的 min 宏都用这种写法,"老者说,"如果没有语句表达式,宏就只能用简单的 ((a) > (b) ? (a) : (b))——这有个致命的问题。"

"什么问题?"

"如果你写 max(x++, y)a > b ? a : b 中的 a 会被求值两次——x++ 执行两次。但用了语句表达式,x++ 只在赋值给 _a 时求值一次。"

老者顿了顿,声音变得低沉:"宏是把双刃剑。用得好,它是利器。用不好,它是暗器。语句表达式让宏变得安全——但前提是你知道它存在。"

四、_builtin 系列 — 编译器的法术

大殿的深处有一间密室。密室的墙上挂满了 开头的函数名——__builtin_popcount

"这些是编译器的内置函数,"老者的声音在密室中回荡,"它们直接映射到硬件指令。不是调用一个函数——而是告诉编译器:生成这条指令。"

他指着 :"Count Leading Zeros。计算一个整数从最高位开始有多少个前导零。在 RISC-V 上,这直接对应 指令。一个时钟周期搞定。"

又指着 :"字节序转换。把 0x12345678 变成 0x78563412。网络协议需要这个——网络用大端序,x86 用小端序。"

最后指着 :"分支预测提示。告诉编译器:这个条件大概率为真(或为假)。编译器会据此调整代码布局——把'热'路径放在一起,把'冷'路径移到远处。"

"这些指令……用户态也能用?"

"当然。GCC 是通用编译器。但内核用得最多——因为内核需要极致的性能,每一纳秒都不能浪费。"

五、likely / unlikely — 告诉编译器你的判断

老者从密室出来,走到大殿的门口。门外有两条路——左边的路宽阔平坦,右边的路狭窄崎岖。

"如果让你选,你猜大多数行人会走哪条?"

"左边。"

老者点头,从袖中掏出两个标签贴在门框上:

c
#define likely(x)   __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

" 告诉编译器:这个条件大概率成立。 告诉编译器:这个条件几乎不会发生。"

他指着门框上贴了 标签的左边:"比如 if (likely(ptr != NULL))——指针通常非空。编译器会把非空的分支放在'热'路径上,指令紧密排列,缓存友好。"

又指着右边:"比如 if (unlikely(err))——错误很少发生。编译器会把错误处理的代码移到远处,不占用宝贵的指令缓存空间。"

林小源想了想:"这不影响正确性?"

"完全不影响。 只改变代码的布局——哪个分支跟在 if 后面,哪个分支被移到远处。程序的逻辑一丝不变。但在微架构层面,这可能带来几个百分点的性能提升。"

老者转身走回大殿,声音渐行渐远:"内核中到处都是 。读懂它们,你就知道哪些路径是正常的、哪些路径是异常的。"

六、section — 控制代码的归宿

大殿的地下室里,有一扇通往不同房间的门。每个房间的门上都标着一个段名:.init.text.exit.text.init.data

老者推开了 .init.text 的门。里面空空荡荡——曾经存放在这里的代码已经消失了。

" 标记的函数会被放到 .init.text 段,"老者说,"内核启动完成后,这块内存会被释放。初始化代码只运行一次,之后就不需要了——留着它只是浪费内存。"

他指着门上的铭文:

c
#define __init    __section(.init.text)
#define __exit    __section(.exit.text)
#define __initdata __section(.init.data)

"用段属性控制代码和数据的存放位置。这是 __attribute__((section)) 的应用。就像修士渡劫后抛弃的旧肉身——完成了使命,便不再需要。"

林小源看着空荡荡的房间,心中涌起一阵莫名的感慨。代码也会被遗忘,也会被释放,也会从内存中消失。

"但有些代码不能消失,"老者补充道,"比如 的处理函数。它必须永远在内存中,因为你不知道系统什么时候会崩溃。"

七、noinline 与 always_inline

老者最后带林小源来到一面告示牌前。牌上写着两种极端的指令:

" 告诉编译器:这个函数必须内联。不管它有多大,不管编译器觉得内联是否划算——必须内联。"老者指着牌上的一行字," 就是 ——它需要操作当前栈帧,不能被真正地'调用'。"

"那 呢?"

"反过来。这个函数绝不内联。"老者的声音低了下来," 就是 。为什么?因为 需要正确的栈回溯——如果它被内联了,调试时就看不到完整的调用栈。"

林小源站在告示牌前,看着那两个对立的指令。一个要求"必须内联",一个要求"绝不内联"。它们看起来矛盾,却各有各的道理。

"内核对内联有精确的控制,"老者说,"不是随心所欲,而是深思熟虑。每一个 的背后,都有一个必须这么做的理由。"

老者转身走向大殿深处,留下林小源一个人站在告示牌前。

灰袍编译器的最后一句话回荡在空气中:

"读懂这些秘法,你才能读懂内核。但记住——秘法的力量在于克制。用对了是利器,用错了是毒药。"


道藏笔记

内核启示

内核代码是 GCC 方言的 C。 它大量使用 GCC 扩展:、语句表达式、。这些不是可选的花哨特性——它们是内核正常工作的基础设施。

如果你想读懂内核源码,先读懂 GCC 手册中的 "C Extensions" 章节。

一个耐人寻味的细节: 属性让内核能够将初始化代码放在 .init.text 段中——这些函数在内核启动完成后会被释放,它们占用的内存会被回收。就像修士渡劫后抛弃的旧肉身,完成了使命便不再需要。

/ 背后的 ,本质上是在告诉 CPU 的分支预测器:哪条路更可能被走。分支预测是现代 CPU 最重要的性能优化之一——但它也有阴暗面。很久以后,当"推测执行漏洞"(Spectre)震惊整个安全界时,人们才意识到:这个看似无害的性能优化,竟然打开了一扇通往数据泄露的大门。

但那是另一个故事了。

代码典籍

c
struct normal {
    char a;
    int b;
};

struct __attribute__((aligned(64))) cache_aligned {
    char a;
    int b;
};

struct __attribute__((packed)) tight {
    char a;
    int b;
};

printf("=== 对齐属性 ===\n");
printf("normal:        %zu 字节\n", sizeof(struct normal));
printf("aligned(64):   %zu 字节\n", sizeof(struct cache_aligned));
printf("packed:        %zu 字节\n", sizeof(struct tight));

printf("\nnormal 偏移:\n");
printf("  a: %zu\n", offsetof(struct normal, a));
printf("  b: %zu\n", offsetof(struct normal, b));

printf("\naligned(64) 偏移:\n");
printf("  a: %zu\n", offsetof(struct cache_aligned, a));
printf("  b: %zu\n", offsetof(struct cache_aligned, b));

printf("\n对齐到 64 字节意味着什么?\n");
printf("  - 结构体大小是 64 的倍数\n");
printf("  - 结构体地址是 64 的倍数\n");
printf("  - 通常用于缓存行对齐,避免 false sharing\n");
c
#define swap(a, b) do {         \
    typeof(a) __tmp = (a);      \
    (a) = (b);                  \
    (b) = __tmp;                \
} while (0)

#define min(x, y) ({                    \
    typeof(x) _min1 = (x);              \
    typeof(y) _min2 = (y);              \
    (void)(&_min1 == &_min2);           \
    _min1 < _min2 ? _min1 : _min2;      \
})

#define max(x, y) ({                    \
    typeof(x) _max1 = (x);              \
    typeof(y) _max2 = (y);              \
    (void)(&_max1 == &_max2);           \
    _max1 > _max2 ? _max1 : _max2;      \
})

int a = 10, b = 20;
printf("=== typeof 与类型安全 ===\n");
printf("before: a=%d, b=%d\n", a, b);
swap(a, b);
printf("after:  a=%d, b=%d\n\n", a, b);

int x = 5, y = 10;
printf("min(%d, %d) = %d\n", x, y, min(x, y));
printf("max(%d, %d) = %d\n", x, y, max(x, y));

double dx = 3.14, dy = 2.71;
printf("min(%.2f, %.2f) = %.2f\n", dx, dy, min(dx, dy));

printf("\n注意: min 宏中的 (&_min1 == &_min2) 检查\n");
printf("如果两个参数类型不同,编译器会产生警告\n");
printf("这是内核的类型安全检查!\n");
c
/* 没有语句表达式的宏:可能有副作用问题 */
#define unsafe_max(a, b) ((a) > (b) ? (a) : (b))

/* 使用语句表达式的宏:安全 */
#define safe_max(a, b) ({      \
    typeof(a) _a = (a);        \
    typeof(b) _b = (b);        \
    _a > _b ? _a : _b;        \
})

/* 更复杂的例子:带日志的宏 */
#define log_max(a, b) ({                        \
    typeof(a) _a = (a);                          \
    typeof(b) _b = (b);                          \
    typeof(a) _result = _a > _b ? _a : _b;      \
    printf("max(%d, %d) = %d\n", _a, _b, _result); \
    _result;                                      \
})

int x = 5, y = 10;

printf("=== 语句表达式 ===\n");

/* 危险: max(x++, y) 会执行两次 x++ */
printf("\nunsafe_max(x++, y):\n");
x = 5;
int r1 = unsafe_max(x++, y);
printf("  结果: %d, x 变成了: %d (x++ 执行了两次!)\n\n", r1, x);

/* 安全: 每个参数只求值一次 */
printf("safe_max(x++, y):\n");
x = 5;
int r2 = safe_max(x++, y);
printf("  结果: %d, x 变成了: %d (x++ 只执行一次)\n\n", r2, x);

/* 可以作为表达式使用 */
int arr[3] = {1, 2, 3};
int idx = log_max(0, 1);
printf("arr[idx] = %d\n", arr[idx]);
c
printf("=== __builtin_ 系列 ===\n\n");

/* 位计数 */
unsigned int val = 0b10101100;
printf("__builtin_clz(0x%x) = %d  (前导零个数)\n",
       val, __builtin_clz(val));
printf("__builtin_ctz(0x%x) = %d  (尾部零个数)\n",
       val, __builtin_ctz(val));
printf("__builtin_popcount(0x%x) = %d  (置位个数)\n",
       val, __builtin_popcount(val));

/* 64 位版本 */
uint64_t val64 = 0x8000000000000000ULL;
printf("\n__builtin_clzll(0x%llx) = %d\n",
       (unsigned long long)val64, __builtin_clzll(val64));

/* 字节序转换 */
uint32_t host_val = 0x12345678;
uint32_t be_val = __builtin_bswap32(host_val);
printf("\n字节序转换:\n");
printf("  主机序: 0x%08x\n", host_val);
printf("  大端:   0x%08x\n", be_val);

/* 期望与不可期望(分支预测提示) */
int x = 42;
if (__builtin_expect(x == 42, 1)) {
    printf("\n__builtin_expect: 编译器会将此分支放在快速路径\n");
}

/* 不可达标记 */
printf("\n__builtin_unreachable(): 标记不可能到达的代码\n");
printf("  编译器会据此优化,不生成该路径的代码\n");
c
#define likely(x)   __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

static int process_data(int data) {
    /* 正常情况: data 有效 */
    if (likely(data > 0)) {
        return data * 2;
    }

    /* 异常情况: data 无效(很少发生) */
    if (unlikely(data < 0)) {
        printf("  警告: 负数输入 %d\n", data);
        return -1;
    }

    /* data == 0 */
    return 0;
}

printf("=== likely / unlikely ===\n\n");

int test_data[] = {42, 17, 3, -1, 0, 88, -5, 100};
int n = sizeof(test_data) / sizeof(test_data[0]);

for (int i = 0; i < n; i++) {
    int result = process_data(test_data[i]);
    printf("  process(%d) = %d\n", test_data[i], result);
}

printf("\nlikely/unlikely 的作用:\n");
printf("  1. 提示 CPU 分支预测器(硬件优化)\n");
printf("  2. 让编译器将 likely 分支放在快速路径\n");
printf("  3. 不影响正确性,只影响性能\n");

printf("\n内核示例:\n");
printf("  if (likely(ptr != NULL))  -- 指针通常非空\n");
printf("  if (unlikely(err))        -- 错误很少发生\n");

破关试炼

编译器秘法试炼

内核用来告诉编译器某个分支大概率会发生的宏是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道