3. 汇编语言速通与AT&T语法:为什么内核开发需要汇编?
说实话,很多做应用开发的朋友一听到汇编就头疼。我刚开始学内核开发时也一样,心想:现在C语言这么强大,编译器优化那么牛,干嘛还要碰汇编?
直到我第一次写启动代码,才发现一个残酷的事实——CPU上电后,连C语言运行时环境都没有。没有栈指针,没有全局变量初始化,你拿什么跑C代码?
嗯,这就是我们必须学汇编的第一个理由。
为什么内核开发离不开汇编?
我总结下来,至少有三个场景你绕不开汇编:
- 启动阶段:CPU刚复位时,内存控制器还没初始化,堆栈都没建立。你得用汇编设置好基本环境,才能跳转到C语言的入口。
- 特权指令操作:像开中断、关中断、修改页表基址这类操作,C语言没有对应的语法。你想想看,
cli(关中断)这种指令,C语言怎么表达? - 性能关键路径:中断响应、上下文切换这些地方,每多一条指令都是开销。我曾在项目中用汇编重写了一个中断处理函数,性能提升了30%。
核心观点:汇编不是用来替代C语言的,而是用来填补C语言做不到的那部分空白。
AT&T语法 vs Intel语法:选哪个?
说到汇编语法,江湖上一直有两大门派。我个人习惯用AT&T语法,原因很简单——GNU工具链默认就是它。你在Linux下用gcc -S生成的汇编代码,全是AT&T风格。
两者的核心区别,我列个表你就明白了:
| 对比项 | AT&T语法 | Intel语法 |
|---|---|---|
| 操作数顺序 | mov src, dst |
mov dst, src |
| 寄存器前缀 | %eax |
eax |
| 立即数前缀 | $0x100 |
0x100 |
| 内存寻址 | disp(base, index, scale) |
[base + index*scale + disp] |
举个例子,把数值0x42写入地址0x1000:
// AT&T语法
movl $0x42, 0x1000
// Intel语法
mov [0x1000], 0x42
你看,AT&T的源操作数在左边,目标在右边。刚开始可能不习惯,但用多了你会发现,这种顺序跟C语言的赋值语句dst = src正好相反——嗯,确实有点反直觉。
我的小技巧:如果你实在记不住顺序,就默念「从哪来到哪去」——源在左,目标在右。
常用指令速通
内核开发用到的汇编指令其实不多。我数了数,常用的也就十几条。下面这几个你务必掌握:
mov:数据搬运工
mov指令是最基础的,就是把数据从一个地方搬到另一个地方。注意后缀:
movb:1字节(8位)movw:2字节(16位)movl:4字节(32位)movq:8字节(64位)
movl $0x1234, %eax // 把立即数0x1234存入eax寄存器
movl %eax, %ebx // 把eax的值复制到ebx
movl (%eax), %ebx // 把eax指向的内存内容读入ebx
jmp:无条件的跳转
这玩意儿就像C语言的goto。内核里经常用它来实现异常处理流程的跳转。
jmp label // 直接跳转到label处
jmp *%eax // 跳转到eax寄存器保存的地址
call和ret:函数调用
这对组合是函数调用的核心。call会把返回地址压栈,然后跳转;ret从栈上弹出返回地址并跳回去。
call my_func // 调用my_func函数
ret // 返回
我曾经在调试一个内核崩溃时,发现就是call和ret不匹配导致的——栈被破坏了,返回地址变成了垃圾数据。那一次排查花了我整整两天。
int:软中断
这是用户态进入内核态的门户。比如Linux的系统调用,就是通过int $0x80或syscall指令触发的。
movl $1, %eax // 系统调用号:exit
movl $0, %ebx // 退出码
int $0x80 // 触发软中断
避坑指南:我曾经在QEMU上调试时,发现int $0x80在64位模式下不好使。后来查资料才知道,64位Linux改用syscall指令了。所以写内核时,一定要搞清楚你跑在什么模式下。
GNU汇编器(as)的使用
GNU汇编器as是GCC工具链的一部分。你写好的汇编文件(.s文件),用as就能编译成目标文件。
基本用法很简单:
# 编译汇编文件
as -o boot.o boot.s
# 链接成可执行文件
ld -o boot.bin boot.o
但这里有个坑——as默认输出的是ELF格式,而我们的内核启动代码需要的是纯二进制格式。所以通常我们会用objcopy来转换:
objcopy -O binary boot.elf boot.bin
下面是一个完整的启动代码示例,展示了汇编在QEMU上的最小工作流程:
.code16 // 告诉汇编器,这是16位代码
.globl _start
_start:
movw $0x7c00, %sp // 设置栈指针
movw $0xb800, %ax // 显存段地址
movw %ax, %es
// 在屏幕上显示一个字符
movb $'H', %es:0
movb $0x07, %es:1 // 白底黑字
// 死循环
loop:
jmp loop
// 填充到512字节,并加上引导扇区标志
.org 510
.word 0xaa55
这个代码虽然简单,但包含了启动代码的核心要素:设置栈、操作硬件、无限循环。你把它编译成二进制文件,放到QEMU里就能跑起来。
调试小技巧:用qemu-system-x86_64 -s -S boot.bin启动,然后另开终端用gdb连接,就可以单步调试汇编代码了。我每次写启动代码都这么干,效率高很多。
本章知识体系
下面这张图帮你理清本章的核心脉络:
说白了,汇编就是你和硬件之间的翻译官。你写C代码时,编译器帮你翻译;但到了启动阶段,你得亲自上阵。掌握了mov、jmp、call、ret、int这五条指令,再加上GNU汇编器的基本用法,你就能在QEMU上跑起第一个内核了。
我刚开始学的时候,也觉得汇编晦涩难懂。但当你看到自己写的几行汇编代码,真的在QEMU屏幕上显示出字符时,那种成就感——嗯,值得你花这个功夫。
公众号:蓝海资料掘金营,微信deep3321