# 第二节 面向机器语言

通用型芯片具有解析并执行一些指令的能力,根据芯片复杂度确定可执行指令数量。对这些指令,我们将其称之为汇编语言。

通常根据芯片厂家的不同及汇编语言编译器的不同,汇编语言可以分为多种类型,比如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], ebpsub 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 上个错误号

# 就业方向

  • 软件破解
  • 编译器
  • 芯片研发