1. 基础
-
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
2. 寻址
- 通用寻址格式:
偏移量(%基址寄存器, %索引寄存器, 比例因子)
;比例因子为1、2、4、8。 - 有些项是可以被省略的,比例因子默认为1。
- 最终地址为:偏移量 + %基址寄存器 + %索引寄存器 x 比例因子。
0x8(%edx)
(%edx, %ecx)
(%edx, %ecx, 4)
0x80(, %ecx, 4)
3. 过程调用
x86-32
寄存器使用惯例
- 由调用者负责保存和恢复的寄存器:
%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
x86-64
寄存器使用惯例
- 由被调用者负责保存和恢复的寄存器:
%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
4. 系统调用
- 通过执行中断
int $0x80
来实现。 - 系统调用号保存在
%eax
中。 - 系统调用参数按序放到
%ebx, %ecx, %edx, %esi, %edi
中。 - 如果参数个数大于 5 个,则所有参数放到一个连续的内存区域中,然后将指向该区域的指针放到
%ebx
中。 - 系统调用返回值放在
%eax
中。
5. 静态链接
- 每个源文件都有自己的代码段和数据段,在程序链接期间,链接器会将多个文件的代码段和数据段集成为单一的代码段和数据段,如此之后,程序便有了一个统一的内存布局。
- 接着将
.o
文件中的符号解析为地址,并将所有的符号引用更新为地址。 - 静态链接库:由多个源文件生成多个
.o
对象文件,然后将这些对象文件打包归档成一个 .a
文件;在静态链接期间,如果 .a
文件中的某个成员能够匹配一个外部符号,则将该成员链接入可执行文件中。
6. Hello, World!
.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!
7. 命令行参数
输出命令行参数:
.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
8. C程序 -> 汇编代码
64 位汇编:
$ gcc -S -O2 hello.c
64 位环境下生成 32 位汇编:
$ sudo apt install g++-multilib
$ gcc -S -O2 -m32 hello.c
9. 反汇编
反汇编对象文件:
$ gcc -c main.c -o main.o -m32 -g
$ objdump -DSr main.o
反汇编可执行文件:
$ gcc -o main main.c -g
$ objdump -DS main
10. 汇编指示
-
.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。
-
.comm buffer, 1000
:声明一个大小为 1000 字节的内存区域,并把区域的起始地址付给 buffer;该内存区域是未初始化的、全局可见的。
-
.lcomm buffer, 1000
:声明一个大小为 1000 字节的内存区域,并把区域的起始地址付给 buffer;该内存区域是未初始化的、只局部可见的。
-
符号:表示数据(变量)或指令(函数)的地址,后跟 :
。
-
.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
11. gcc 内联汇编
基本格式:
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)
: "b"(a), "c"(b)
:
);
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;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)