x86 的寄存器为32位,x64 的寄存器为64位。寄存器间对应关系:
64位寄存器 低32位 低16位 低8位
rax eax ax al
rbx ebx bx bl
rcx ecx cx cl
rdx edx dx dl
rsi esi si sil
rdi edi di dil
rbp ebp bp bpl
rsp esp sp spl
r8 r8d r8w r8b
r9 r9d r9w r9b
r10 r10d r10w r10b
r11 r11d r11w r11b
r12 r12d r12w r12b
r13 r13d r13w r13b
r14 r14d r14w r14b
r15 r15d r15w r15b
在引用寄存器时,需要加上 %
前缀,如,%rax
。
立即数以 $
作为前缀,如,$0x1
。
源操作数在前,目的操作数在后。
指令后缀
后缀 大小(字节)
b 1
w 2
l 4
q 8
如,
movl $0x4, %eax # %eax = $0x4
偏移量(%基址寄存器, %索引寄存器, 比例因子)
;比例因子为1、2、4、8。0x8(%edx)
(%edx, %ecx)
(%edx, %ecx, 4)
0x80(, %ecx, 4)
寄存器使用惯例
%eax, %edx, %ecx
;%ebx, %esi, %edi
;%eax
保存返回值。栈帧
%esp
指向栈顶(低地址),%ebp
指向栈帧(高地址),%ebp ~ %esp
之间的区域就是栈帧。%ebp
向 %esp
):
%ebp
);# set up
pushl %ebp # 旧的 %ebp
movl %esp, %ebp # 新的 %ebp
pushl %ebx # 保存用到的寄存器
movl 12(%ebp), %ecx # 第二个参数
movl 8(%ebp), %ecx # 第一个参数
...
# finish
movl -4(%ebp), %ebx # 恢复用到的寄存器
movl %ebp, %esp
popl %ebp
ret
寄存器使用惯例
%rbx, %rbp, %r10, %r12, %r13, %r14, %r15
;%rdi, %rsi, %rdx, %rcx, %r8, %r9
寄存器中,这些寄存器由调用者负责保存和恢复;如果参数大于六个,则余下参数还是从右往左压入栈;%rax
保存返回值。栈帧
完全基于 %rsp
完成。
# 保存用到的寄存器
movq %rbx, -16(%rsp)
movq %r12, -8(%rsp)
# 分配栈帧
subq $16, %rsp
...
# 恢复用到的寄存器
movq (%rsp), %rbx
movq 8(%rsp), %r12
# 释放栈帧
addq $16, %rsp
int $0x80
来实现。%eax
中。%ebx, %ecx, %edx, %esi, %edi
中。%ebx
中。%eax
中。.o
文件中的符号解析为地址,并将所有的符号引用更新为地址。.o
对象文件,然后将这些对象文件打包归档成一个 .a
文件;在静态链接期间,如果 .a
文件中的某个成员能够匹配一个外部符号,则将该成员链接入可执行文件中。.data
msg:
.string "Hello, World!\n"
len = .-msg
.text
.global _start
_start:
movl $4, %eax # 系统调用号,write 为 4
movl $1, %ebx # fd = 1,表示 stdout
movl $msg, %ecx # buf = $msg
movl $len, %edx # count = $len
int $0x80
movl $1, %eax
movl $0, %ebx
int $0x80
64 位汇编:
$ as -o helloworld.o helloworld.s
$ ld -o helloworld helloworld.o
$ ./helloworld
Hello, World!
64 位环境下生成 32 位汇编:
$ as --32 -o helloworld.o helloworld.s
$ ld -m elf_i386 -o helloworld helloworld.o
$ ./helloworld
Hello, World!
输出命令行参数:
.text
.global _start
_start:
popl %ecx # argc
next_arg:
popl %ecx # argv[i]
test %ecx, %ecx # argv[i] == NULL
jz exit
movl %ecx, %ebx
xorl %edx, %edx # strlen(argv[i])
strlen:
movb (%ebx), %al
inc %edx
inc %ebx
test %al, %al
jnz strlen
movb $10, -1(%ebx) # ascii('\n') == 10
movl $4, %eax
movl $1, %ebx
int $0x80
jmp next_arg
exit:
movl $1, %eax
xorl %ebx, %ebx
int $0x80
$ ./arg hello world
./arg
hello
world
64 位汇编:
$ gcc -S -O2 hello.c
64 位环境下生成 32 位汇编:
$ sudo apt install g++-multilib
$ gcc -S -O2 -m32 hello.c
反汇编对象文件:
$ gcc -c main.c -o main.o -m32 -g
# -D: Display assembler contents of all sections
# -S: Intermix source code with disassembly
# -r: Display the relocation entries in the file
$ objdump -DSr main.o
反汇编可执行文件:
$ gcc -o main main.c -g
$ objdump -DS main
.text
:代码段。
.data
:带有初始值的数据段;声明一个数据元素时,需要指明数据类型及初始值;因为它具有初始值,所以它在可执行文件中实实在在具有指定大小的空间来保存这些值。
.bss
:不带有初始值的数据段;声明一个数据元素时,不需要指明数据类型及初始值,只需声明保留一段内存即可;因为它不具有初始值,所以它在可执行文件中并没有分配指定大小的空间,而是在程序运行时才分配声明的内存。
.rodata
:只读数据段。
.section
:把代码划分为若干个区,当程序被加载进内存时,不同的区会被加载到不同的地方,且不同的区具有不同的读、写、执行权限;例如,.section .data
、.section .text
。
.p2align 4
:按 2 的 4 次方,即 16 字节对齐。
.align 4
:按 4 字节对齐。
.string "hello, world!"
:定义一个字符串,内容为 “hello, world!”。
.long 10
:定义一个 long 类型的数据,值为 10。
.global main
:表示 main
符号是全局可见的(同一程序的其他模块可访问),如果没有 .global
修饰,则该符号不是全局可见的。
.equ SYS_OPEN, 5
:定义一个符号常量 SYS_OPEN,其值为 5。
.include "record.s"
:包含 record.s 文件至当前文件。
.rept n xxx .endr
:重复 n 次 xxx。
# 重复 31 次 .byte 0
# 用作数据填充
.rept 31
.byte 0
.endr
基本格式:
asm [volatile] (
assembler template
: [output operands]
: [input operands]
: [list of clobbered registers]
)
volatile
告诉 gcc 不要优化掉这些汇编代码。
每条汇编指令需要放在 "assembler template"
中,且需要以 \n
结尾。
在汇编代码中,如果使用到寄存器,则需要使用两个 %
,如,%%eax
。
操作数约束:在输出、输入操作数中,可以通过操作数约束来限制操作数应该放在哪个寄存器或直接使用内存:"约束"(变量)
。
+---+--------------------+
| r | Register(s) |
+---+--------------------+
| a | %eax, %ax, %al |
| b | %ebx, %bx, %bl |
| c | %ecx, %cx, %cl |
| d | %edx, %dx, %dl |
| S | %esi, %si |
| D | %edi, %di |
| r | 任何一个通用寄存器 |
| m | 使用内存 |
+---+--------------------+
修饰符 =, +, &
只能用于输出部分;=
表示当前输出表达式的属性为只写,+
表示当前输出表达式的属性为可读可写,&
告诉 gcc 不得为任何输入操作表达式分配与此输出操作表达式相同的寄存器。
数字占位符:在汇编代码中,可以使用 %0, %1, ...
来依次引用输出、输入中的寄存器。
#include <stdio.h>
int main() {
int a = 1;
int b = 2;
int c;
asm volatile (
"addl %1, %2\n"
"movl %2, %0\n"
: "=a"(c) // 变量 c 放在寄存器 eax(%0) 中
: "b"(a), "c"(b) // 变量 a, b 分别放在 eax(%1) 和 ebx(%2) 中
:
);
printf("c = %d\n", c);
return 0;
}
名称占位符:此时表达式的格式为 [name] "约束"(变量)
,在汇编代码中可以使用 %[name]
来引用该操作数。
int main() {
int a = 1;
int b = 2;
int c;
asm volatile (
"addl %[a], %[b]\n"
"movl %[b], %[c]\n"
: [c] "=a"(c)
: [a] "b"(a), [b] "c"(b)
:
);
printf("c = %d\n", c);
return 0;
}
clobbered registers 列表告诉 gcc 我们会使用和修改这些寄存器(不包括输入、输出寄存器),如,"eax, ecx"
;另外,如果汇编代码修改了内存,则还需要加上 memory
约束。
#include <stdio.h>
void my_memset(void* buf, char c, size_t count) {
asm volatile (
"cld\n"
"rep stosb\n"
:
: "a"(c), "D"(buf), "c"(count)
: "memory"
);
}
int main() {
char buf[8];
my_memset(buf, 'a', sizeof(buf));
for (int i = 0; i < sizeof(buf); i++) {
printf("%c", buf[i]);
}
printf("\n");
return 0;
}