2.4 汇编与C混合编程实战


2.4 汇编与 C 混合编程实战

本节摘要:接口开发经常需要在 C 的骨架上嵌几段汇编——为的是精确到拍的延时、直接操控端口的指令、以及复用现成的底层代码;本节拆开函数调用的栈帧,讲清两种语言互相调用的全部约定。

前三节把处理器的"硬"的一面讲完了:寄存器、时序、地址变换。本节是本章的软着陆,也是接口工程师真正的日常——设备驱动的主体用 C 写,但总会遇到几个必须下到汇编的角落。先看懂调用约定,再谈什么时候值得下去。

从一行 C 代码挖出两层语言

看一段最普通的调用:

unsigned char port_in(unsigned int port); /* 声明:从指定端口读一个字节 */ status = port_in(0x61); /* 调用:读系统端口状态 */

编译器把这次调用翻成 8086 语境下的机器动作大致是:把实参压入栈、把返回地址压入栈、跳到函数体;函数体开头把栈顶指针挪一半给栈帧基址,腾出局部变量区;结尾恢复现场、弹出返回地址、跳回去。每一句都对应 2.1 节的某件道具——SP 记栈顶、BP 记栈帧、IP 记返回位置。所谓调用约定,就是双方对"参数放哪、谁保存谁"的事先约定:约定不匹配,轻则参数读错位,重则栈被搬空、返回地址失踪。

图 2-2:一次调用发生后的栈帧布局

图 2-2:一次调用发生后的栈帧布局

手写一个被 C 调用的汇编函数

以"读端口"为例,看完整闭环。汇编一侧(语法以主流汇编器为准,示意为主):

; 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 立骨架,用汇编镶牙"。

问:内联汇编的输入输出约束是什么意思? 就是告诉编译器"这段汇编要吃哪个变量、吐给哪个变量",让它替你安排寄存器与驻留。约束写对,编译器还能做寄存器分配优化;写错,轻则算错重则崩栈——所以能写成独立汇编函数的,尽量独立成函数,边界清楚,约定好查。

本节要点回顾

  • 调用约定三环节:参数位置、现场保护、返回值寄存器,两边必须逐条对齐。
  • 栈帧结构:实参、返回地址、旧 BP、局部区依次排列,ret 的安全性全系于栈的完整。
  • 内联与独立函数的分界:三行以内嵌内联,成段逻辑独立成函数。
  • 软件延时的边界:精确短延时用汇编,长延时与等待就绪交给硬件定时器与中断。

下一章把镜头从处理器转向它的另一半:存储系统——数据住在哪、为什么要分层、Cache 如何填平速度的鸿沟。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U