与 C 语言互操作(C Interop)
汇编与 C 协作有两种方式:独立汇编文件(编译后与 C 目标文件链接,本模块重点)与内联汇编(C 源码内嵌汇编,详见模块八)。
C 基本类型与汇编对应
x86-64 Linux(LP64 模型)下常见 C 类型的大小及传递方式:
| C 类型 | 大小 | 寄存器传递 | 返回位置 |
|---|---|---|---|
char / signed char |
1 | %dil 等低 8 位 |
%al |
short |
2 | %di 等低 16 位 |
%ax |
int |
4 | %edi 等 32 位 |
%eax |
long / 指针 |
8 | %rdi 等 64 位 |
%rax |
float |
4 | %xmm0–%xmm7 |
%xmm0 |
double |
8 | %xmm0–%xmm7 |
%xmm0 |
注意
long在 Windows 上是 4 字节——本知识库只针对 Linux System V ABI。
被 C 调用的汇编函数
C 侧只需声明原型,汇编侧遵守调用约定(参数寄存器、返回值 %rax、不破坏 callee-saved,见模块六):
# add.s —— 被 C 调用的汇编函数
.section .note.GNU-stack,"",@progbits
.text
.globl add
.type add, @function
add:
leaq (%rdi,%rsi), %rax # return a + b
ret
.size add, . - add
/* main.c */
#include <stdio.h>
long add(long a, long b);
int main(void) {
printf("3+4=%ld\n", add(3, 4));
return 0;
}
gcc main.c add.s -o prog && ./prog
要点:
- 汇编符号没有名字修饰,C 里直接引用原名;
static函数对应.local。 .type/.size让工具链生成正确的符号类型与调试信息。- C 编译器会假设你遵守 ABI;破坏 callee-saved 寄存器或栈对齐会导致难以定位的崩溃。
从汇编调用 C 函数(含 libc)
直接 call 函数名(外部函数经 @PLT,见模块十二):
# 包装 libc 的 strlen
.section .note.GNU-stack,"",@progbits
.text
.globl str_len
.type str_len, @function
str_len:
call strlen@PLT # rdi 原样传入,rax 返回长度
ret
.size str_len, . - str_len
gcc main.c str_len.s -o prog # libc 自动链接
调用前记住:%rdi 等传参、call 前 %rsp 16 字节对齐、变参函数(printf 等)%al=0(见模块六)。
什么时候用 libc、什么时候直接 syscall? 格式化输出(printf)、malloc/free、线程、字符串函数等重功能交给 libc;直接 syscall(模块七)适合教学、极小程序和不想链接 libc 的场景。两种风格可以混用:比如自己用 syscall 写文件、再用 libc 的 printf 打印日志。
全局变量互访
汇编导出数据,C 引用:
# counter.s
.section .note.GNU-stack,"",@progbits
.data
.globl asm_count
asm_count: .quad 0 # C 侧: extern long asm_count;
.text
.globl bump
.type bump, @function
bump:
incq asm_count(%rip) # RIP 相对访问自己的全局变量
ret
.size bump, . - bump
/* main.c */
#include <stdio.h>
extern long asm_count; /* 引用汇编里的全局变量 */
void bump(void);
int main(void) {
bump(); bump(); bump();
printf("%ld\n", asm_count); /* 3 */
return 0;
}
反过来,汇编引用 C 的全局变量只需正常使用符号(未定义符号由链接器解析):movl c_var(%rip), %eax。
结构体布局与偏移
按 C 的对齐规则手工定义偏移(.equ),用基址+偏移访问字段:
struct S { char c; long l; int i; };
/* x86-64 布局:c@0, l@8, i@16, sizeof=24(末尾按 8 字节补齐) */
.equ S_C, 0
.equ S_L, 8
.equ S_I, 16
.equ S_SIZE, 24
# long get_l(struct S *s) { return s->l; }
get_l:
movq S_L(%rdi), %rax # s->l
ret
规则:每个字段对齐到自身大小的倍数(long 对齐 8,int 对齐 4),结构体总大小对齐到最大字段的对齐值。拿不准时用 C 写个小程序打印 sizeof/offsetof(见模块十四的工具配合)。
混合项目的构建
# 逐个编译再链接
as func.s -o func.o
gcc -c main.c -o main.o
gcc main.o func.o -o prog
# 或让 gcc 一步驱动(.s/.c 混传)
gcc main.c func.s -o prog
# 不链接 libc 的纯汇编项目
gcc -nostdlib -no-pie start.s -o prog
内联汇编 vs 独立文件
| 场景 | 选择 |
|---|---|
| 少量指令、需要编译器优化配合 | 内联汇编(模块八) |
| 完整函数、清晰调试、复用多个翻译单元 | 独立 .s 文件 |
| 性能关键且需要精确控制寄存器 | 独立 .s 文件(配合 -O2 编译其余部分) |