汇编语法格式
在汇编中,一般有两种不同的语法,它们是AT&T语法和Intel语法。
AT&T语法是GNU汇编器(GAS)所采用的语法,主要应用于UNIX系统。AT&T语法的特点是:
操作数的顺序是源操作数在前,目的操作数在后,同时使用“%”来表示寄存器。例如:
movl $0x12345678, %eax # 把常数0x12345678赋给EAX寄存器
Intel语法则是Intel的汇编器采用的语法,主要应用于Windows系统。Intel语法的特点是操作数的顺序是目的操作数在前,源操作数在后,同时使用“[]”来表示内存地址。例如:
mov eax, 0x12345678 ; 把常数0x12345678赋给EAX寄存器
调用约定
在x86架构下,常见的C调用约定有以下几种:
cdecl:参数从右往左入栈,由调用方负责恢复堆栈,返回值通常存在eax寄存器中,适用于可变参数的函数。
stdcall:参数从右往左入栈,由被调用方负责恢复堆栈,返回值通常存在eax寄存器中,适用于Win32 API。
fastcall:将前两个整型或指针类型的参数放在ecx和edx寄存器中,其他参数从右往左入栈,由被调用方负责恢复堆栈,返回值通常存在eax寄存器中,适用于微软Visual C++编译器。
cdecl64:参数从右往左入栈,由调用方负责恢复堆栈,返回值通常存在rax寄存器中,适用于x86-64架构。
System V ABI:参数从左往右依次放在寄存器和栈中,返回值通常存在eax/rax寄存器中,由被调用方负责恢复堆栈,适用于Linux、macOS等系统。
寄存器
x86-64 指令架构要求至少有 16 个 64Bit 的通用寄存器。
64位 | 32位 | 16位 | 8位 | 全称 | 用途 |
---|---|---|---|---|---|
通 | 用 | 寄 | 存 | 器 | |
%rax | %eax | %ax | %al/%ah | register a extended | 存储函数的返回值(return value) |
%rbx | %ebx | %bx | %bl/%bh | register b extended | 没有约定 |
%rcx | %ecx | %cx | %cl/%ch | register c extended | 存储函数的第4个参数 |
%rdx | %edx | %dx | %dl/dh | register d extended | 存储函数的第3个参数 |
%rsi | %esi | %si | - | register source index | 存储函数的第2个参数 |
%rdi | %edi | %di | - | register destination index | 存储函数的第1个参数 |
%r8 | %r8d | %r8w | %r8b | register 8 | 存储函数的第5个参数 |
%r9 | %r9d | %r9w | %r9b | register 9 | 存储函数的第6个参数 |
%r10-%r11 | %r10d-%r11d | %r10w-%r11w | %r10b-%r11b | register 10 ~ register 11 | / |
%r12-%r15 | %r12d-%r15d | %r12w-%r15w | %r12b-%r15b | register 12 ~ register 15 | / |
堆 | 栈 | 寄 | 存 | 器 | |
%rbp | %ebp | %bp | - | register base pointer | 存储栈底指针 |
%rsp | %esp | %sp | - | register stack pointer | 存储栈顶地址 |
指 | 令 | 寄 | 存 | 器 | |
rip | eip | 永远存储下一条指令的地址 | |||
段 | 寄 | 存 | 器 | ||
CS | 代码段寄存器 | ||||
SS | 栈段寄存器 | ||||
DS | 数据段寄存器 | ||||
ES | 附加(数据)段寄存器 | ||||
FS | 数据段寄存器 | ||||
GS | 数据段寄存器 |
提示:
ax分为ah和al,高低位之分,可以独立使用。
为了保证兼容性,四个寄存器都可以分为两个独立的8位寄存器使用;
ax可以分为ah和al
bx可以分为bh和bl
cx可以分为ch和cl
dx可以分为dh和dl
函数调用分析
参数既可以放在栈中传递,也可以直接使用寄存器传递。
调用以call开始,以ret结束,因此可以定位call和ret来判别函数调用。
下面举一个例子说明:
说明:设a=3、b=1,计算a-b,a、b为字型数据;
参数:进入子程序时,栈顶存放段基地址IP,后面依次存放参数a和b;
结果:a-b=2
# 调用方将参数b=1放入ax寄存器中
mov ax,1
# 调用方将ax入栈
push ax
# 调用方将参数a=3放入ax寄存器中
mov ax,3
# 调用方将ax入栈
push ax
# 跳转到子程序
call child
# 被调用方child,将调用方的段基地址IP入栈,所以被调用方帮调用方入栈个参数
push bp
# 被调用方将自己的段基地址设为当前栈顶
mov bp,sp
# 因为在push参数a和b后,子程序又push了一个bp,所以bp+2是段基地址,bp+4是参数a
mov ax, [bp+4]
# bp+6参数b,计算a-b,并将结果存在寄存器ax上
sub ax, [bp+6]
# 弹出调用方的bp值,设定BP:IP寄存器,并回退两个参数的位置4
ret 4
常用指令
# 入栈,保存上一个栈帧的rbp到栈中
push rbp
# 更改rsp指针为rbp
mov rbp, rsp
# rsp的地址减去20h,为存储数据做准备
sub rsp, 20h
# 保存上一个栈帧的edi值
mov [rbp+argc], edi
# 保存上一个栈帧的rsi值
mov [rbp+argv], rsi
# 将当前栈帧的入参aHello字符串的地址装载到rsi寄存器中
lea rsi, aHello
# 将cout对象地址加载到rdi中
lea rdi, _ZSt4cout@@GLIBCXX_3_4
# 跳转到相应函数
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
内存段
内存本身没有段,但是可以通过规定寄存器来人为给内存分段,也就是说,使用不同段寄存器访问不通的内存段。
程序被划分为指令、数据和栈三部分。指令就是指令,数据就是数据,栈就是程序的执行进度。
# 指令段内存地址
CS:IP
# 数据段内存地址
DS、[adrress]
# 栈段读写地址
SS:SP
参考资料
AMD64 Architecture Programmer’s Manual Volume 1: Application Programming
Computer Systems: A Programmer’s Perspective, 3/E (CS:APP3e)