7.3 C与汇编混合编程接口


7.3 C 与汇编混合编程接口

本节摘要:C 与汇编互调的全部规则就是第 5 章的 AAPCS 合同加上两条款:汇编函数要正确声明全局符号与类型,C 侧要给出匹配的原型声明。本节给出双向互调的完整模板,演示从 C 看汇编、从汇编看 C 两种视角的符号表,并附三大接口报错的诊断表。

工具与调试都齐了,还剩最后一道接口:让 C 与汇编真正互相调用。这件事没有任何新知识——第 5 章的调用合同就是全部规则——难点全在"两边都摆对姿势"的细节上。本节的目标是让你拿到一对互调的模板,配上一张报错诊断表,此后混合编程不再需要搜索。

汇编暴露给 C:三条规矩

汇编函数要被 C 调用,汇编侧守三条规矩。规矩一,符号要导出:.global sum 声明全局可见,.type sum, %function 标注这是函数(链接器与调试器靠它区分代码与数据)。规矩二,遵守 AAPCS:参数从 x0 起、返回值放 x0、用到的 x19x28x29x30 自己保存——这份名单在 5.2 节手推过。规矩三,栈对齐入乡随俗:进入函数时 sp 是十六字节对齐的(C 调用方保证),你若压栈,保持每步对齐即可。一个可直接抄的模板:

.text .global vec_add3 // long vec_add3(long a, long b, long c) .type vec_add3, %function vec_add3: add x0, x0, x1 add x0, x0, x2 // 三个参数全在寄存器,加完返回,零栈操作 ret

C 侧只需要一句原型声明,告诉编译器"这个符号存在且长这样":

long vec_add3(long a, long b, long c); // 与汇编签名一致,参数个数别写错 int main(void) { return (int)vec_add3(1, 2, 3); }

编译链接各一条命令,gcc 驱动器认得汇编后缀会自动调度汇编器——混合工程其实不比纯 C 工程多几个步骤。

C 暴露给汇编:看懂编译器的产物

反方向的调用不需要 C 做任何事——编译器生成的函数天然符合 AAPCS——需要做功课的是汇编作者:你得知道 C 函数的签名。两个办法。办法一,读头文件推寄存器分配:long scale(long x, long k)xx0kx1、返回值 x0。办法二,让编译器自己交底:对任何拿不准的 C 函数,-S 编一份看汇编,参数怎么传、返回放哪、哪些寄存器被它动了——一目了然。后者在结构体参数场景尤其必要:小结构体塞进寄存器对、大结构体改走内存(5.2 节的隐形第一参数),纸上推不如看产物。

// 调用 C 的函数:long scale(long x, long k) bl scale // x0 x1 已备好参数,返回值在 x0 // 若调用点在寄存器敏感区,注意 scale 可能改写全部临时寄存器

第三句注释值得加粗:C 函数有权改写所有调用方保存寄存器x0x17x30),你在 bl 之前若把重要值放在临时寄存器里,回来就没了——要么先压栈,要么挪进被保存寄存器(挪了就要在序言里保存它)。这条是混合编程事故榜第一名。

图 7-4:互调时的寄存器交接台

图 7-4:互调时的寄存器交接台

实验与变式:把接口跑穿

综合实验:写一个 C 程序调用汇编的 vec_add3,汇编里再调用 C 的 printf 打印结果——双向互调一次跑通。链接阶段若有问题,按上一节的三病因流程查。跑通后的两个变式加深理解:变式一,在汇编函数里故意不保存就改写 x19,观察调用方(C)在后续打印出一个不相关的脏值——破坏合同的后果当场可视化;变式二,把参数从三个加到十个,反汇编看第九、十个参数怎么经栈传递,用调试器在栈上找到它们的值——5.2 节"超编参数走调用方栈"的实锤。

深挖:结构体参数的寄存器旅行与可见性开关

结构体参数怎么走寄存器,是混合编程里最常查错的地方,直接做一个对账实验。按 AAPCS:尺寸 16 字节以内的结构体按字段打包进寄存器对(一个 8 字节加 4 字节的组合占 x0w1),更大的整块走内存——调用方预留、x8 传地址。让编译器交底最直观:

struct Small { long a; int b; }; // 传参:x0 装 a,w1 装 b struct Big { long v[4]; }; // 传参:x8 指向调用方预留的副本 gcc -O2 -S caller.c 反汇编 bl 处: Small 版:mov x0, ... ldr w1, ... bl handle Big 版: add x8, sp, #16 bl handle_big (x8 是隐形指针)

返回结构体是同一套规则镜像:小结构体经 x0x1 回来,大的由调用方预留、x8x8 出。手写汇编收发结构体的全部秘密就是这一页。另一枚值得认识的旋钮是符号可见性:默认全局符号在动态链接场景可能被同名符号拦截(调用意外落进别的库),对内部函数加 static 或可见性声明收窄暴露面——接口柜里不仅要接得对,还要关得严。

常见问答

问:汇编里能不能直接调用 printf 这类变参函数? 答:能,但 variadic 调用有附加条款(5.2 节提过的栈记录),最稳妥的路径是写一个固定原型的 C 包装函数(收固定个数参数、内部转调 printf),汇编只调包装。混合工程里"复杂接口包一层 C"是省心又省 bug 的通用套路。
把本节的模板再压成一句可带走的操作序列:汇编侧三行头(global、type、函数标号)加正文加 ret,C 侧一句原型,链接一条命令;出问题按诊断表从上往下查。这份序列建议原样存进自己的代码片段库——混合编程的接口知识五年不变,变的只是项目里给它套的外壳。

本节要点回顾

  • 三条规矩:导出符号、守 AAPCS、保持栈对齐——汇编暴露给 C 的全部成本。
  • 读签名两种法:读头文件推寄存器,或直接 -S 让编译器交底,结构体参数必用后者。
  • 临时寄存器不可信:跨 bl 调用后 x0x17 随时作废,重要值自己保存。
  • 诊断三板斧:nm 查符号、file 查架构、查修饰名,顺序按成本排。
  • 破坏要可视化:故意违反保存责任并观察脏值,一次实验胜过十次提醒。

到这一节为止,工具、调试、接口全部配齐。最后一章谈性能与规范——把正确的代码变成又快又稳的代码。


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