简单(陋)的x86汇编语言教程
啥是汇编语言?
汇编语言其实不算是某一个语言,而是一种”human readable machine code”。比起高级语言更接近底层的机器语言,所以也更加复杂和冗长。汇编语言是直接传给cpu的指令,所以与cpu的架构非常相关。
汇编语言有以下几个特征:
- 和寄存器交互。每个寄存器有不同的目的,但存储的长度是相同的,也和cpu架构相关,32-bit的机器,寄存器就是32-bit长的。
- 和栈交互。栈是一个LIFO(last in, first out)的数据结构。在内存中,其实就看作一个数组,因为可以访问任意位置(random access)。栈的主要实现是通过一个栈顶指针(存储在寄存器esp中)。pop和push就以这个指针为参考。
常量类型
db: 1 byte
dw: 2 byte
dd: 4 byte
x86汇编中有三种可以声明的常量类型(或者只有我那么叫?),他们的主要区别在于长度不同。
db
: 1 字节, 包括十六进制数(hexidecimal, e.g.0xff
), 字符('s'
), 1个字节能表示的整形(100
)dw
: 2 字节,更大的整型dd
: 4 字节,更更大的整型
基本语法
以操作符开头,后面是涉及到的运算单位,用逗号分隔。
operation [operands, ...]
🌰 :
mv ebx, 123 ; ebx = 123
mv ebx, ecx ; ebx = ecx
add ebx, ecx ; ebx += ecx
mul ebx ; eax *= ebx 注意,乘法除法默认在eax上操作
寄存器与栈
一些重要的特殊功能寄存器。
EIP
在计算机中,cpu读取的每一条指令,其实也是存储在内存中的数据(这种指令和数据的纠葛不清也带来了很多安全问题,比如buffer overflow,但这些就跑远了)。cpu通过读取EIP所储存的内存地址中的数据来知道下一条指令是什么,同样也可以通过覆写EIP来实现跳转。跳转可以应用在许多场景中,如果是向前跳转就是分支结构(if),向后跳转就是循环(while),向某个地址跳转也可以是函数调用等等。下面是个简单的例子:
_start:
...// first execute some code here
jmp skip
...// never gets executed
skip:
...// second execute some code here
栈与ESP
如上文所说,栈就是一块有LIFO性质的内存。ESP寄存器储存是栈顶的位置,所以pop相当于把ESP位置上的内容取出,然后ESP += 4
, 即往下移动4个byte。push同理。
push 100 ; [100][][][][] stack top
^(esp)
push 302 ; [100][302][][][]
^(esp)
sub esp 4 ; 先向上移动esp,再给esp所在位置赋值,和push是等价的
mov [esp], dword 345; [100][302][345][][]
^(esp)
pop eax ; [100][302][345][][]
^(esp)
; 把栈顶元素储存到eax中。此时其实只移动了esp,栈的元素没有变化
mov eax, dword [esp]; 这两个操作和pop等价
add esp, 4
函数调用
函数调用和栈与ESP,EIP都息息相关。首先函数调用使用的是一个新的操作:call
。它做的事情包括:
- 把当前EIP的值(即这个命令的地址)push到栈中
- 执行一个jump,覆写EIP为跳转到函数的地址
_start:
... ; things before function
call func ; call function
... ; things after function
func:
... ; things in the function
pop eax ; get the return address from the stack
jump eax ; jump back to the caller
我们最后通过pop
和jump
操作来获取并跳转到返回的地址。也可以使用ret
来代替这两个指令。注意到,这意味着在函数执行结束后,我们一定要保证栈顶元素就是返回地址,否则就不知道jump
到哪里去了。一个普遍的做法是使用”base pointer”, 即EBP,来保证ret的是正确地址。EBP保存着在进入函数时的栈顶元素(即返回地址)。
func:
mov ebp, esp ; 在进入函数时,把栈顶元素存到ebp中
... ; do whatever
mov esp, ebp ; 即将整个函数调用栈都清空,保证栈顶时返回地址
ret ; ret一定合法
但这个操作还有一个问题:函数中又调用了函数,比如这种执行顺序, 会使得外层函数的EBP被内层函数覆盖掉,没法返回正确的地址。
mov ebp, esp ; function 1 called
call func2 ; function 1 call function 2
mov ebp, esp ; function 2 called
... ; function 2 execute
mov esp, ebp ; function 2 returns
ret ;
mov esp, ebp ; function 1 returns, but ebp has already been overwritten
简单的处理办法是,把EBP也给push到栈上,然后在函数返回时pop。
push ebp ; 储存着caller的返回地址,这样就保证不会因为覆盖ebp而丢掉
mov ebp, esp ; 常规操作,保存当前函数的返回地址
... ; do whatever
mov esp, ebp ; 清空栈
pop ebp ; 还原caller的ebp
ret
对应到栈的情况就是
push ebp ; [return_address][previous_ebp][][][] ebp=previous_ebp
mov ebp, esp ; ebp updated
... ; [return_address][previous_ebp][stuffs][][]
mov esp, ebp ; [return_address][previous_ebp][][][]
pop ebp ; [return_address][][][][] ebp=previous_ebp
ret ;
mov esp, ebp; pop ebp
也可以用leave
代替。
函数传参和返回
函数的caller在调用函数之前,可以把参数push到栈上。所以栈的最终构成是这样的:
栈顶(ESP) |
---|
函数的本地变量 (callee push) |
previous EBP(即caller的返回地址, callee push) |
return address (caller push) |
函数参数 (caller push) |
一个简单的例子, 考虑只有一个参数(4 byte)的函数
caller:
push 1 ; argument is 1
call func
func:
push ebp ; push previous_ebp
mov ebp, esp ;
mov eax, [ebp+8]; 获取参数
; +8是因为要越过previous_ebp和caller push的return address
; 如果是第n个参数,就是 epb+8+4*(n-1)
其它的操作符介绍
其实主要想讲LEA
。LEA(Load Effective Address)看字面意思比较难以捉摸。
The LEA instruction loads an address. If you have some varaible, you can load the address of it into a register and manipulate the data indirectly with the register as a pointer. LEA does not change any flags.
简单来说就是把一个变量的地址存到寄存器中,可以通过这个寄存器来间接修改这个变量的值。不就是指针嘛!LEA
把变量的地址存到寄存器,MOV
把变量的数据存到寄存器。
语法: lea dst, src
例子:
variable db 123
_start:
lea eax, variable ; 把variable的地址存到eax中
mov byte ptr [eax], 345 ; 通过eax修改variable
代码分析示例
看看这段代码做了啥事
push ebp ; 进入函数
mov ebp, esp ;
push ebx ; ebx push到栈上
sub esp, 0x8 ; esp -= 8,相当于开了8个字节的buffer
push dword ptr [ebp + 0x8] ; ebp + 0x8 就是第一个参数的位置,push到栈上,作为strcpy函数参数
lea edx=>local_10, [ebp + -0xc]; 把ebp - 12的地址存到edx中,注意ebp - 12就是buffer的开始
push edx ; edx push到栈上,作为strcpy函数参数
mov ebx, eax ;
call strcpy
...
所以上述操作就是
void func(char *param){
char local_10[8];
strcpy(local_10, param);
}