本节摘要:接口开发经常需要在 C 的骨架上嵌几段汇编——为的是精确到拍的延时、直接操控端口的指令、以及复用现成的底层代码;本节拆开函数调用的栈帧,讲清两种语言互相调用的全部约定。
前三节把处理器的"硬"的一面讲完了:寄存器、时序、地址变换。本节是本章的软着陆,也是接口工程师真正的日常——设备驱动的主体用 C 写,但总会遇到几个必须下到汇编的角落。先看懂调用约定,再谈什么时候值得下去。
看一段最普通的调用:
unsigned char port_in(unsigned int port); /* 声明:从指定端口读一个字节 */ status = port_in(0x61); /* 调用:读系统端口状态 */
编译器把这次调用翻成 8086 语境下的机器动作大致是:把实参压入栈、把返回地址压入栈、跳到函数体;函数体开头把栈顶指针挪一半给栈帧基址,腾出局部变量区;结尾恢复现场、弹出返回地址、跳回去。每一句都对应 2.1 节的某件道具——SP 记栈顶、BP 记栈帧、IP 记返回位置。所谓调用约定,就是双方对"参数放哪、谁保存谁"的事先约定:约定不匹配,轻则参数读错位,重则栈被搬空、返回地址失踪。

以"读端口"为例,看完整闭环。汇编一侧(语法以主流汇编器为准,示意为主):
; unsigned char port_in(unsigned int port) ; 约定:参数在 BP 加 4 的偏移处,返回值放 AL _port_in: PUSH BP ; 保存调用者栈帧基址 MOV BP, SP ; 建立自己的栈帧 MOV DX, [BP+4] ; 取实参 port 到 DX(IN 指令只认 DX) IN AL, DX ; 从端口读一个字节 POP BP ; 拆除栈帧 RET ; 弹返回地址返回,AL 即返回值
C 一侧只要声明与调用即可。三个约定环节逐一核对:参数入栈的次序与宽度(这里实参是 16 位端口号,占 BP 加 4 起的两个字节);现场保护(本函数只动了 BP 与 DX,BP 必须保,DX 属于可牺牲寄存器则不必);返回值位置(8 位返回值放 AL)。混合编程的全部秘密就是把这三条核对清单走一遍——不同编译器的约定细节有差异,动手前查一眼文档,别靠猜。
至于 C 里嵌汇编(内联汇编),适合一两句指令的小场合:写内联汇编的输入输出约束,让编译器替你分配寄存器。它的好处是不破坏调用约定,坏处是语法与编译器强绑定。我的偏好是:三行以内用内联,再多的逻辑独立写成汇编函数——可读性的账更划算。
背景:某 LED 驱动需要一个约 2 毫秒的软件延时,初版用 C 的空循环实现,在不同优化等级下实测延时长短漂移,最短时只有预期的一半。操作:把延时函数改用汇编手写,循环体固定为三条指令,并按 2.2 节的时序手工核算:每条指令的周期数乘以循环次数,加上调用开销。结果:同一份编译产物,延时稳定落在目标值附近,误差来自中断插入而非代码本身。解读:C 空循环的周期数是编译器的自由裁量——优化等级一变,循环可能被削减、展开或改用更快的寻址方式;而汇编把每拍钉死。需要精确时间的场合,代码必须自己掌管节拍。 变式:延时再长就该换硬件定时器(第 6 章 8253 的本职工作),软件延时只适合微秒级的短场景;等待外设就绪也不该烧循环,第 6 章的中断与 DMA 会给出正解。
工程判断的标准很简单:C 表达不了或表达不稳的时刻才下去。三类典型场景:直接端口与特殊指令(IN、OUT、开关注中断标志的专用指令);精确到拍的时序例程(短延时、位脉冲);与固件或历史代码的对接(对方只提供了汇编符号)。反过来说,逻辑控制、数据处理、协议组织这些活,C 的表达力与可维护性完胜,硬写汇编纯属自找麻烦。接口工程师的成熟标志,是能一眼判断"这段该待在哪一层语言里"。
把"参数在栈里"落实成算术。进入被调函数时,返回地址在栈顶;执行压 BP 后,BP 获得当前 SP 的值——此刻 BP 指向的是旧 BP 的位置。于是参数的坐标是固定的:BP 加二处是返回地址,BP 加四处是第一个参数(入栈次序与约定有关,压栈式约定里先压的参数在更高地址)。第二个参数就在 BP 加六,依此类推。手算一遍这个偏移链,混编调试里"参数读错位"的问题从此有了显微镜——断在函数入口,看 BP、看栈内容、对照偏移,三步定案。
混合编程最容易翻车的是"谁保存谁"的约定。一张表钉在墙上:
| 寄存器 | 责任方 | 缘由 |
|---|---|---|
| BP | 被调函数必须保 | 它是栈帧的门牌,动了 caller 就找不回家 |
| SP | 硬件与约定共管 | 压弹必须配平,进出栈字节数严格相等 |
| SI DI BX | 按约定,多数要求被调方保 | 编译器把它们当跨语句资产使用 |
| AX CX DX | 被调方可自由使用 | 调用方默认它们是"用完即弃"的耗材 |
| 标志寄存器 | 按返回约定 | 若用进位传状态,返回前就不能破坏它 |
多数争议都出在中间两行。经验法则是:写完汇编函数,把所有动过的寄存器列个清单,对照约定逐个标注"要保"还是"可弃"——这道工序花两分钟,能省掉数小时的单步追凶。
问:为什么不用纯汇编写驱动,性能不是更高吗? 驱动代码的九成时间在管理逻辑与状态,汇编在这些地方毫无优势,反而放大出错面。真正吃性能的只有极少数时序敏感片段——正是本节教的那几类。成熟的工程判断是"用 C 立骨架,用汇编镶牙"。
问:内联汇编的输入输出约束是什么意思? 就是告诉编译器"这段汇编要吃哪个变量、吐给哪个变量",让它替你安排寄存器与驻留。约束写对,编译器还能做寄存器分配优化;写错,轻则算错重则崩栈——所以能写成独立汇编函数的,尽量独立成函数,边界清楚,约定好查。
下一章把镜头从处理器转向它的另一半:存储系统——数据住在哪、为什么要分层、Cache 如何填平速度的鸿沟。