综合示例(Examples)

示例 1:Hello World(64 位,纯系统调用)

.section .rodata
msg:    .ascii "Hello, AT&T asm!\n"
.set len, . - msg

.section .text
.globl _start
_start:
    movq $1, %rax            # write
    movq $1, %rdi            # stdout
    leaq msg(%rip), %rsi
    movq $len, %rdx
    syscall

    movq $60, %rax           # exit
    xorq %rdi, %rdi
    syscall
as hello.s -o hello.o && ld hello.o -o hello && ./hello

示例 2:求 1..N 之和(函数 + 调用)

.section .text
.globl _start
_start:
    movq $100, %rdi
    call sum_to_n
    # 结果在 rax
    movq %rax, %rdi          # 作为 exit code
    movq $60, %rax
    syscall

# long sum_to_n(long n) { long s=0; for(long i=1;i<=n;i++) s+=i; return s; }
sum_to_n:
    xorq %rax, %rax          # s = 0
    movq $1, %rcx            # i = 1
1:
    cmpq %rdi, %rcx          # 计算 rcx - rdi
    jg   2f                  # if i > n 结束
    addq %rcx, %rax
    incq %rcx
    jmp  1b
2:
    ret

1b 表示向(backward)找标号 12f 表示向(forward)找标号 2

示例 3:数组最大值

# long max(long *a, long n)
#   rdi = a, rsi = n
max:
    movq (%rdi), %rax        # result = a[0]
    movq $1, %rcx            # i = 1
1:
    cmpq %rsi, %rcx          # rcx - rsi
    jge  2f                  # if i >= n 结束
    movq (%rdi,%rcx,8), %rdx
    cmpq %rax, %rdx          # rdx - rax
    jle  3f
    movq %rdx, %rax          # result = a[i]
3:
    incq %rcx
    jmp  1b
2:
    ret

示例 4:C 内联汇编求最大值

#include <stdio.h>
int main(void) {
    long a = 7, b = 12, m;
    asm ("movq %1, %0\n\t"     /* m = a */
         "cmpq %2, %0\n\t"     /* m - b */
         "cmovl %2, %0"        /* 若 a < b,则 m = b */
         : "=r"(m)
         : "r"(a), "r"(b));
    printf("max=%ld\n", m);
    return 0;
}

逻辑:先把 a 放入 m;比较 m - b,若 m < bcmovl)则 m = b,最终 m = max(a,b)。 注意 m 必须先初始化——cmov 条件不成立时不会写目的寄存器。

示例 5:递归阶乘(栈帧实战)

.section .text
.globl _start
_start:
    movq $10, %rdi
    call fact
    movq %rax, %rdi          # 10! = 3628800
    movq $60, %rax
    syscall

# long fact(long n) { return n<=1 ? 1 : n*fact(n-1); }
fact:
    pushq %rbp
    movq  %rsp, %rbp
    subq  $16, %rsp
    movq  %rdi, -8(%rbp)     # 保存 n(rdi 是 caller-saved,递归会破坏)
    cmpq  $1, %rdi
    jg    1f
    movq  $1, %rax           # n <= 1 返回 1
    jmp   2f
1:  decq  %rdi
    call  fact               # rax = fact(n-1)
    imulq -8(%rbp), %rax     # rax = n * fact(n-1)
2:  leave                    # = movq %rbp,%rsp; popq %rbp
    ret

要点:%rdi 是 caller-saved,递归调用会覆盖它,所以先把 n 存到自己的栈帧(-8(%rbp)),调用返回后再取出来用。

示例 6:汇编函数 + C 主程序混合编译

汇编文件 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

C 文件 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 同时驱动 C 编译与汇编):

gcc main.c add.s -o prog && ./prog    # 3+4=7

要点:C 侧只需声明函数原型;汇编侧保证遵守调用约定(参数寄存器、返回 %rax、不破坏 callee-saved),.type/.size 让工具链生成更好的调试/回溯信息。

示例 7:打印整数(十进制转换)

纯系统调用程序无法用 printf,输出数字需要自己把整数转成十进制字符串:

.section .rodata
ten: .quad 10
.section .bss
.lcomm outbuf, 24          # 64 位十进制最长 20 位 + 换行,留足余量
.section .text
.globl _start
_start:
    movq $123456789, %rdi
    call print_int
    movq $60, %rax
    xorq %rdi, %rdi
    syscall

# void print_int(unsigned long n) —— rdi = n,打印十进制数 + 换行
print_int:
    movq %rdi, %rax            # 被除数 = n
    leaq outbuf+23(%rip), %rcx # 从缓冲区末尾向前写
    movb $10, (%rcx)           # 末尾先放换行符
    decq %rcx
1:  xorq %rdx, %rdx            # div 前必须清 rdx(被除数高位)
    divq ten(%rip)             # rax /= 10,余数在 rdx
    addb $'0', %dl             # 余数 0-9 -> ASCII '0'-'9'
    movb %dl, (%rcx)
    decq %rcx
    testq %rax, %rax           # 商为 0 则结束
    jnz  1b
    incq %rcx                  # 回退到第一个数字字符
    leaq outbuf+23(%rip), %rax
    subq %rcx, %rax            # 输出长度(含换行)
    movq %rax, %rdx
    movq %rcx, %rsi
    movq $1, %rax              # write(1, buf, len)
    movq $1, %rdi
    syscall
    ret

要点:从低位向高位生成数字、从缓冲区末尾向前写,写完再整体输出;div 前清 %rdx 的老规矩又见面了。

调试建议

  • gdb 单步:gdb ./hellolayout asmstepiinfo registers。详见模块十四
  • objdump -d -M intel 反查生成的机器码。
  • strace ./hello 观察系统调用。
  • 编译时加 -g 保留调试信息(仅 GAS 配合 gcc 驱动时)。

练习清单(按难度递进)

每个练习都建议在 GDB 里单步过一遍(模块十四),卡住时查模块十八

  1. 改 Hello World:打印自己的名字;把 .ascii 换成 .asciz 观察长度计算的差异。
  2. 求 1..N 和:N 用 .set 定义,结果作为退出码(注意低 8 位限制,用示例 7print_int 打印验证)。
  3. 手写 strlen:给定 %rdi 指向的字符串,循环找结尾 0,返回长度;与 libc 的 strlen 对比(模块十六)。
  4. 手写 memcpy/memset:分别用普通循环和 rep movsb/rep stosb 实现,体会模块十一
  5. 数组遍历:定义 .long 数组,求和、求最大值(对照示例 3),把结果打印出来。
  6. 冒泡排序:对 .long 数组排序后逐个打印;体会双层循环与下标计算。
  7. 递归:阶乘(对照示例 5)或斐波那契;用 GDB bt 观察递归调用栈。
  8. 读输入回显:用 read 从 stdin 读一块数据再 write 回去(模块七),注意处理返回值。
  9. 跳转表菜单:读入一个数字,用跳转表(模块五)分发到不同处理分支。
  10. 与 C 互操作:把第 3、4 题的函数做成独立 .s 文件,写 C 主程序调用并比对结果(模块十六)。

完成 1–5 题即掌握基本语法与调试;完成 6–10 题后可以认为已经上手 AT&T 汇编。