# 第二节 面向机器语言
通用型芯片具有解析并执行一些指令的能力,根据芯片复杂度确定可执行指令数量。对这些指令,我们将其称之为汇编语言。
通常根据芯片厂家的不同及汇编语言编译器的不同,汇编语言可以分为多种类型,比如Intel系列x86指令集编译器有masm32(用于Windows平台的汇编编译器)、nasm(用于linux平台的汇编编译器)等。
# 概览
汇编语言是一类语法简单,操控灵活,但难以开发,容易出BUG的语言,特性有:
- 直接与机器交互,它支持的功能就是机器能实现的功能,所以需要全面
- 计算方式简单独立,每条指令都只能是一个特定步骤,通过不同的组合从而实现复杂操作
# x86(IA-32)
此处我们以C语言及masm32汇编举例,说说汇编语言语法,让大家有一个初步的映像:
// 定义函数
int test () {
// 执行计算,并放入局部变量
int a = 5 + 4;
// 返回局部变量
return a;
}
等价汇编:
; 定义函数
push ebp
mov ebp, esp
; 执行计算,并放入局部变量
sub esp, 4
mov eax, 5
add eax, 4
mov dword ptr [ebp], eax
; 返回局部变量
mov eax, dword ptr [ebp]
mov esp, ebp
pop ebp
ret
下面我来给个解读一下这样的代码。首先对于定义函数部分,C语言通过函数头、函数体的方式来定义,语法为:
返回类型 函数名 (参数列表) {
函数体
}
在汇编里面呢?可以看到,函数结构几乎没有了。因为CPU可不管你是不是函数,它只会按照逻辑顺序执行。我们悉知的将一些抽象的、公共的功能,提取为函数的方法,在汇编语言里,其实是,保存环境、执行函数代码、恢复环境。
可以看到汇编语言里,函数开头及结束,有这样的代码:
push ebp
mov ebp, esp
; ...
mov esp, ebp
pop ebp
ret 0
我们来逐个解读。
应用程序栈也是一种栈,栈先进后出,同时应用程序的栈向上生长。
push ebp
,含义为取出栈基址指针(Base Pointer)并将其压入(Push)栈(Stack)中。此处的栈,意为应用程序栈。比如我们有耳闻的著名异常:stack overflow(栈溢出)即指的这个栈。ebp这词在不同位数下具有不同写法。16位8086指令集里写作bp(Base Pointer),32位x86里写作ebp(Extended Base Pointer)64位amd64里写作rbp(不是RBQ)。
push ebp
这个指令等价于两条指令组合:mov dword ptr [esp], ebp
、sub esp, 4
。我们向栈存数据时,首先将数据移动至栈顶指针指向的内存区域,然后将栈顶指针-4(以字节为单位访问内存时,32位数据占4字节)。
然后是mov ebp, esp
,这句意思是将esp内容移动至ebp。由于我们进入了一个新的函数环境,老环境暂时不用了,于是我们将栈顶至栈底的空间封存,重置这两个指针为栈上方未使用到的空间,这样就能使得函数环境隔离,避免函数内部运行影响外部调用者。
好。然后是函数末尾,上面两句反过来,可以理解为,函数执行完毕,恢复回之前的调用者环境。最后一句ret 0
代表将函数执行地址转为调用者所在地址。后接的数字代表参数占用空间字节数。一般一个参数占用4字节,这个数字在32位环境一般是参数数量*4这样子。
然后是函数体部分代码:
; ...
; 执行计算,并放入局部变量
sub esp, 4
mov eax, 5
add eax, 4
mov dword ptr [ebp], eax
; 返回局部变量
mov eax, dword ptr [ebp]
; ...
sub esp, 4
,这句代码含义是,栈顶向上空余出4字节,代表这部分空间有变量需要用到,也就是局部变量a。由于函数释放时,直接恢复调用者的栈空间,局部变量直接被释放,所以不需要担心释放的问题(RAII后面再说)。
mov eax, 5
,这句含义为,向寄存器eax中存入一个立即数(immediate)。
add eax, 4
,这与代码的含义为,将寄存器eax中现存的值+4。
mov dword ptr [ebp], eax
,这句代码含义为,将寄存器存储的值移动至ebp指针指向的内存地址,也就是我们计算完5+4后,将计算结果移动至a变量指代的内存区域。
mov eax, dword ptr [ebp]
,这句看起来和上一句作用相反,但含义不同了。这儿指的是将变量a的值取出来,放置在eax寄存器中。我们一般默认eax为函数返回值。
# 程序运行环境
我们继续使用Win32举例。应用软件一般运行于应用层(Ring3,除此之外还有内核层Ring0)。每个进程互相隔离(也就是访问相同的地址不一定能访问到相同的内容,这儿是虚拟地址概念)。每个进程有5个段:
- CS(Code Segment)代码段,这段内存区域用于存放代码
- DS(Data Segment)数据段,用于存放应用程序数据
- ES(Extra Segment)扩展段,用于存放应用程序附加数据
- FS(Flag Segment)标识段,用于存放进程标识
- SS(Stack Segment)栈段,用于存放函数调用返回地址及函数局部变量
其中FS段信息如下:
偏移 | 说明 |
---|---|
0x00 | 指向SEH链指针 |
0x04 | 线程栈顶部 |
0x08 | 线程栈底部 |
0x0C | SubSystemTib |
0x10 | FiberData |
0x14 | ArbitraryUserPointer |
0x18 | FS段寄存器在内存中的镜像地址 |
0x20 | 进程PID |
0x24 | 线程ID |
0x2C | 指向线程局部存储指针 |
0x30 | PEB结构(进程结构)地址 |
0x34 | 上个错误号 |
# 就业方向
- 软件破解
- 编译器
- 芯片研发