逆向工程——汇编基础(五)

子程序

在汇编语言中,无门并不能像高级语言中那样,在不和程序其他部分起冲突的情况下,定义和其他主程序或子程序一样的变量名。
汇编语言并不注重程序员的负担,它依赖程序员的良好设计,以期发挥CPU的最佳性能。
汇编语言不是结构化的语言,它不提供直接的“局部变量”。如果需要,只能通过堆或栈自行实现

参数的传递是靠寄存器堆栈来完成的。高级语言中,子程序(函数/过程,或者类似概念的东西)依赖于堆和栈来传递。

子程序的执行过程

无论是C/C++/BASIC/Pascal,子程序的执行过程基本都是一致的。

  • 调用者将子程序执行完成时应返回的地址/参数压入堆栈。
  • 子程序使用BP指针+偏移量对栈中的参数寻址,并取出,完成操作。
  • 子程序使用RETRETF指令返回。此时,CPU将IP置为堆栈中保存的地址,并继续予以执行。

毋庸置疑,堆栈在整个过程中发挥着非常重要的作用。不过,本质上对子程序最重要的还是返回地址,如果子程序不知道这个地址,那么系统将会崩溃。
调用子程序的指令是CALL,对应的返回指令是RET,另外还有ENTERLEAVE,她们可以帮助进行堆栈的维护。

CALL指令的参数是被调用子程序的地址。使用宏汇编的时候,这通常是一个标号。CALL和RET,一级ENTER和LEAVE配对,以实现对堆栈的自动操作,而不需要程序员进行PUSH/POP,以及跳转的操作,从而提高了效率。

下面一段程序,是从作者文档中抄下来的,我搭建环境并且编译之后,并没有得到相同的汇编代码,所以大家不用拘泥于下面的细节,但是可以从原作者的分析中得到一些有用的知识。

程序是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
int myTransform(int nInput){
return (nInput*2+3) %7;
}

int main(int argc, char* argv[])
{
int a[3];
for(register int i=0;i<3;i++){
a[i] = myTransform(i);
}
return 0;
}

作者编译出来,并且合并了C++代码和汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
int myTransform(int nInput){
//00401000 push ebp ;保护现场原先的EBP指针
//00401001 mov ebp,esp
return (nInput*2+3)%7;
//00401003 mov eax,dword ptr [nInput] ;取参数
//00401006 lea eax,[eax+eax+3] ;LEA比ADD加法更快
//0040100A cdq ;DWORD -> QWORD(扩展字长)
//0040100B mov ecx,7 ;除数
//00401010 idiv eax,ecx ;除
//00401012 mov eax,edx ;商->eax(eax中保存返回值)
}

//00401014 pop ebp ;恢复现场的EBP指针
//00401005 ret ;返回

//; 此处删除10条int 3指令,不会影响程序行为

int main(int argc, char* argv[])
{
//00401020 push ebp ;保护现场原先的EBP指针
//00401021 mov ebp,esp
//00401023 sub esp,10h ;为取argc/argv修正堆栈指针

int a[3];
for(register int i=0;i<3;i++){
//00401026 mov dword ptr [i],0 ;0->i
//0040102D jmp main+18h(00401038) ;判断循环条件
//0040102F mov eax,dword ptr [i] ;i->eax
//00401032 add eax,1 ;EAX++
//00401035 mov dword ptr [i],eax ;eax -> i
//00401038 cmp dword ptr [i],3; 循环条件:i与3比较
//0040103C jge main+33h(00401053) ;如果不符合条件,则应结束循环
a[i] = myTransform[i];
//0040103E mov ecx,dword ptr [i] ;i->ECX
//00401041 push ecx ;ecx(i) -> 堆栈
//00401042 call myTransform(00401000) ;调用myTransform
//00401047 add esp,4 ;ESP+4:在堆中的新单元

//;准备存放返回结果
//0040104A mov edx,dword ptr [i] ;i->EDX
//0040104D mov dword ptr [edx*4],eax ;将EAX(即myTransform的返回值)放回a[i]

}
//00401051 jmp main+0Fh(0040102F) ;计算出i++,并继续循环
return 0;
//00401053 xor eax,eax ;返回值应该是0

}
//00401055 mov esp,ebp ;恢复堆栈指针
//00401057 pop ebp ;恢复BP
//00401058 ret ;返回调用者(C++运行环境)
1
00401003 mov eax,dword ptr [nInput]		;取参数

这句话在另外一个反汇编器得到的结果是:

1
00401003 mov eax,dword ptr [ebp+8]		;取参数

剖析上面的程序只是说明了前面所提到的子程序的一部分用法。对于汇编语言来说,完全没有必要拘泥于结构化程序设计的框架。

中断

中断应该说是一个陈旧的话题。这新的系统中,它的作用正在逐渐被削弱,而变成操作系统专用的东西。并不是所有的计算机系统都提供中断,然而这x86系统中,它的作用是不可替代的。

中断实际上是一类特殊的子程序。它通常由系统调用,以响应突发事件。

例如,进行磁盘操作时,为了提高性能,可能会使用DMA方式进行操作。CPU向DMA控制器发出指令,要求外设和内存直接交换数据,而不通过CPU。然后,CPU转去进行其他的操作;当数据交换结束时,CPU可能需要进行一些后续操作,但此时它如何才能知道DMA已经完成了操作呢?

很显然不是依靠CPU去查询状态——这样DMA的优势就不明显了。为了尽可能地利用DMA的优势,这完成DMA操作的时候,DMA会告诉CPU“这事儿我办完了”,然后CPU会根据需要进行处理。

这种处理可能很复杂,需要若干条指令来完成。子程序是一个不错的主意,不过,CALL指令需要指定地址,让外设强迫CPU执行一条CALL指令也违背了CPU作为核心控制单元的设计初衷。考虑到这些,这x86系统中引入了中断向量的概念。

中断向量表是保存在系统数据区(实模式下,是0:0开始的一段区域)的一组指针。这组指针指向每个中断服务程序的地址。整个中断向量表的结构是一个线性表。

每个中断服务都有自己的唯一编号,我们通常称之为中断号。每一个中断号对应中断向量表中的一项,也就是一个中断的向量。外设向驼背发出中断请求,而CPU自己将根据当前的程序状态决定是否中断当前程序并调用相应的中断服务。

不难根据造成中断的原因将中断分为两类:硬件中断软件中断。硬件中断有很多分类方法,如根据是否可以屏蔽分类、根据优先级高低分类,等等。考虑到这些分类并不一定科学,并且对于我们介绍中断的使用没有太大的帮助,因此我们并不打算太详细地介绍它。

在设计操作系统时,中断向量的概念曾经带来过很大的便利。操作系统随时可能升级,这样,通过CALL来调用操作系统的服务(如果说每个程序都包含对于文件系统、进程表这些应该由操作系统管理的数据的直接操作的话,不仅会造成程序的臃肿,而且不利于系统的安全)就显得不太合适了——没人知道以后的操作系统的服务程序入口点会不会是那。软件中断的存在为解决这个问题提供了方便。

对于一台包含了BIOS的计算机来说,启动的时候系统已经提供了一部分服务,例如显示服务。无论你的BIOS、显卡有多么的“个性”,只要他们和IBM PC 兼容,那么此时你肯定可以通过调用16(10h)号中断来使用显示服务。调用中断的指令是:

1
int 中断号

这将引发CPU去调用一个中断。CPU将保存当前的程序状态字,清除Trap和Interrupt两个标志,将即将执行的指令地上压入堆栈,并调用中断服务(根据中断向量表)。

编写中断服务程序不是一件容易的事情。很多时候,中断服务程序必须写成可重入代码(或纯代码,pure code)。所谓可重入代码是指,程序的运行过程中可以被打断,并由开始处再次执行,并且执行结果不受影响。

由于在多线程环境中等其他一些地方进行程序设计时也需要考虑这个因素,因此这里着重说一个可重入代码的编写。

可重入代码最主要的要求就是,程序不应使用某个指定的内存地址的内存(对于高级语言来说,这通常是全局变量,或对象的成员)。如果可能的话,应使用寄存器,或其他方式来解决。如果不能做到这一点,则必须在开始、结束的时候分别禁止和启用中断,并且,运行时间不能太长。

调用软件中断时,通常都是通过寄存器传进、传出参数。这意味着你的int指令周围也许会存在一些“帮手”,比如下面的代码:

1
2
mov ax,4c00h
int 21h

就是通过调用DOS中断服务返回父进程,并带回错误找反馈码0。其中,ax中的数据4c00h就是传递给DOS中断服务的参数。

到此,x86汇编语言的基础部分就讲完了。

在以后的课程中,我们将学习另一本书,叫做《RE4B》。

作业

  1. 在GitHub的Issue里面有一个关于逆向工程的虚拟机环境,有下载链接,请准备好。
  2. 熟悉上面的程序与汇编的对应。
  3. 学习笔记。