与 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 编译其余部分)