第三章:编译器秘法
涉及内核源码: ,
楔子
林小源理解了指针和结构体,但他很快发现了新的问题。
他看到一段代码,里面写着 typeof(x)——这不是一个函数,也不是一个变量,它在编译时就被替换了。他看到另一段代码,__attribute__((aligned(64)))——这像是某种指令,但它不在汇编里,而是在 C 代码中。
这些是什么?
他开始观察编译器的行为。他发现,编译器不仅仅是把 C 代码翻译成机器指令——它会重排代码的顺序,会内联小函数,会删除永远不会执行的分支。
更让他惊讶的是,编译器有一些"秘密指令"——GCC 扩展。这些扩展不是标准 C 的一部分,但内核大量使用它们。
获取表达式的类型。 控制变量和函数的属性。 系列直接映射到硬件指令。
这些是编译器的秘法。
掌握它们,你才能读懂内核中那些"看起来不像 C"的代码。
一、aligned 与 packed — 松紧自如
林小源第一次见到编译器,是在一座古老的大殿里。
大殿的主人是一个老者,身披灰袍,手中握着一把巨大的锤子——编译器的符号。他的眼睛深不可测,仿佛能看穿一切代码的本质。
"你来了。"老者的声音回荡在大殿中,"你想读懂内核的代码?那得先学会我的语言。"
林小源环顾四周。大殿的地板上刻着各种属性声明——、、、——像是某种古老的符文。
老者指向地板上两块相邻的区域。左边的区域紧凑得像一块压缩饼干,右边的区域宽敞得像一座宫殿。
"同样的两个字段——char a 和 int b——用 ,它们紧紧贴在一起,5 字节。用 aligned(64),它们被拉伸到 64 字节的边界上。"老者敲了敲地板,"packed 用于网络协议头、硬件寄存器这些必须精确控制布局的场景。aligned 用于缓存行对齐——避免多核之间的 false sharing。"
"false sharing?"
"两个 CPU 核心各自修改不同的变量,但这两个变量恰好在同一个缓存行里。结果就是——每次修改都要通知对方的缓存失效,白白浪费时间。用 把热数据对齐到缓存行边界,就能避免这个问题。"
林小源蹲下来仔细看那两块区域。紧凑的那一块严丝合缝,没有一丝浪费;宽敞的那一块虽然浪费了空间,但每一块数据都恰好落在它该在的位置上。
"松紧之间,各有各的道理。"老者说。
二、typeof — 类型的反射
老者带着林小源来到一面镜子前。镜子不是反射光线的——它反射的是类型。
"这是 ,"老者说,"GCC 扩展。它在编译时获取表达式的类型,但不求值。"
林小源把一个 int x 举到镜子前。镜子里映出了 int。他把一个 double d 举过去,镜子里映出了 。
"有什么用?"
老者从袖中掏出一张纸条,上面写着一个宏:
#define swap(a, b) do { \
typeof(a) __tmp = (a); \
(a) = (b); \
(b) = __tmp; \
} while (0)"没有 ,你写 宏就得用固定的类型——比如 int。但有了 , 可以用于任何类型。int、、——都行。"
林小源接过纸条,翻到背面。那里写着另一个宏:
#define min(x, y) ({ \
typeof(x) _min1 = (x); \
typeof(y) _min2 = (y); \
(void)(&_min1 == &_min2); \
_min1 < _min2 ? _min1 : _min2; \
})"等等,"林小源指着中间那行,"(&_min1 == &_min2) 是什么?比较两个指针?"
老者笑了:"那是类型安全检查。如果 x 和 y 的类型不同,_min1 和 _min2 的类型就不同,它们的指针比较会产生编译器警告。这个宏在编译期就阻止了类型不匹配——而不会在运行时给你一个莫名其妙的结果。"
三、语句表达式 — 宏的利器
老者又从袖中掏出另一张纸条。上面的宏更复杂了:
#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 — 告诉编译器你的判断
老者从密室出来,走到大殿的门口。门外有两条路——左边的路宽阔平坦,右边的路狭窄崎岖。
"如果让你选,你猜大多数行人会走哪条?"
"左边。"
老者点头,从袖中掏出两个标签贴在门框上: 和 。
#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 段,"老者说,"内核启动完成后,这块内存会被释放。初始化代码只运行一次,之后就不需要了——留着它只是浪费内存。"
他指着门上的铭文:
#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)震惊整个安全界时,人们才意识到:这个看似无害的性能优化,竟然打开了一扇通往数据泄露的大门。
但那是另一个故事了。
代码典籍
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");#include <stdio.h>
#include <stddef.h>
struct normal {
char a;
int b;
};
struct __attribute__((aligned(64))) cache_aligned {
char a;
int b;
};
struct __attribute__((packed)) tight {
char a;
int b;
};
int main() {
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");
return 0;
}#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");#include <stdio.h>
#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 main() {
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");
return 0;
}/* 没有语句表达式的宏:可能有副作用问题 */
#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]);#include <stdio.h>
/* 没有语句表达式的宏:可能有副作用问题 */
#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 main() {
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]);
return 0;
}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");#include <stdio.h>
#include <stdint.h>
int main() {
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");
return 0;
}#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");#include <stdio.h>
#include <stdint.h>
#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;
}
int main() {
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");
return 0;
}编译器秘法试炼
内核用来告诉编译器某个分支大概率会发生的宏是什么?