高级语言程序的汇编解析
在高级语言中,如C 和PASCAL 等等,我们不再直接对硬件资源进行操作,而是面
向于问题的解决,这主要体现在数据抽象化和程序的结构化。例如我们用变量名来存取数
据,而不再关心这个数据究竟在内存的什么地方。这样,对硬件资源的使用方式完全交给
了编译器去处理。不过,一些基本的规则还是存在的,而且大多数编译器都遵循一些规
范,这使得我们在阅读反汇编代码的时候日子好过一点。这里主要讲讲汇编代码中一些和
高级语言对应的地方。
1. 普通变量。通常声明的变量是存放在内存中的。编译器把变量名和一个内存地址联
系起来(这里要注意的是,所谓的“确定的地址”是对编译器而言在编译阶段算出的一个临
时的地址。在连接成可执行文件并加载到内存中执行的时候要进行重定位等一系列调整,
才生成一个实时的内存地址,不过这并不影响程序的逻辑,所以先不必太在意这些细节,
只要知道所有的函数名字和变量名字都对应一个内存的地址就行了),所以变量名在汇编
代码中就表现为一个有效地址,就是放在方括号中的操作数。例如,在C 文件中声明:
int my_age;
这个整型的变量就存在一个特定的内存位置。语句 my_age= 32; 在反汇编代码中可能
表现为:
mov word ptr [007E85DA], 20
所以在方括号中的有效地址对应的是变量名。又如:
char my_name[11] = "lianzi2000";
这样的说明也确定了一个地址,对应于my_name. 假设地址是007E85DC,则内存中
[007E85DC]='l',[007E85DD]='i', etc. 对my_name 的访问也就是对这地址处的数据访问。
指针变量其本身也同样对应一个地址,因为它本身也是一个变量。如:
char *your_name;
这时也确定变量"your_name"对应一个内存地址,假设为007E85F0. 语句
your_name=my_name;很可能表现为:
mov [007E85F0], 007E85DC ;your_name 的内容是my_name 的地址。
2. 寄存器变量
在C 和C 中允许说明寄存器变量。register int i; 指明i 是寄存器存放的整型变量。通
常,编译器都把寄存器变量放在esi 和edi 中。寄存器是在cpu 内部的结构,对它的访问
要比内存快得多,所以把频繁使用的变量放在寄存器中可以提高程序执行速度。
3. 数组
不管是多少维的数组,在内存中总是把所有的元素都连续存放,所以在内存中总是一
维的。例如,int i_array[2][3]; 在内存确定了一个地址,从该地址开始的12 个字节用来存
贮该数组的元素。所以变量名i_array 对应着该数组的起始地址,也即是指向数组的第一个
元素。存放的顺序一般是i_array[0][0],[0][1],[0][2],[1][0],[1][1],[1][2] 即最右边的下标变化最
快。当需要访问某个元素时,程序就会从多维索引值换算成一维索引,如访问
i_array[1][1],换算成内存中的一维索引值就是1*3 1=4.这种换算可能在编译的时候就可以
确定,也可能要到运行时才可以确定。无论如何,如果我们把i_array 对应的地址装入一个
通用寄存器作为基址,则对数组元素的访问就是一个计算有效地址的问题:
; i_array[1][1]=0x16
lea ebx,xxxxxxxx ;i_array 对应的地址装入ebx
mov edx,04 ;访问i_array[1][1],编译时就已经确定
mov word ptr [ebx edx*2], 16 ;
当然,取决于不同的编译器和程序上下文,具体实现可能不同,但这种基本的形式是
确定的。从这里也可以看到比例因子的作用(还记得比例因子的取值为1,2,4 或8
吗?),因为在目前的系统中简单变量总是占据1,2,4 或者8 个字节的长度,所以比例因
子的存在为在内存中的查表操作提供了极大方便。
4. 结构和对象
结构和对象的成员在内存中也都连续存放,但有时为了在字边界或双字边界对齐,可
能有些微调整,所以要确定对象的大小应该用sizeof 操作符而不应该把成员的大小相加来
计算。当我们声明一个结构变量或初始化一个对象时,这个结构变量和对象的名字也对应
一个内存地址。举例说明:
struct tag_info_struct
{
int age;
int sex;
float height;
float weight;
} marry;
变量marry 就对应一个内存地址。在这个地址开始,有足够多的字节(sizeof(marry))容
纳所有的成员。每一个成员则对应一个相对于这个地址的偏移量。这里假设此结构中所有
的成员都连续存放,则age 的相对地址为0,sex 为2, height 为4,weight 为8。
; marry.sex=0;
lea ebx,xxxxxxxx ;marry 对应的内存地址
mov word ptr [ebx 2], 0
......
对象的情况基本相同。注意成员函数具体的实现在代码段中,在对象中存放的是一个
指向该函数的指针。
5. 函数调用
一个函数在被定义时,也确定一个内存地址对应于函数名字。如:
long comb(int m, int n)
{
long temp;
.....
return temp;
}
这样,函数comb 就对应一个内存地址。对它的调用表现为:
CALL xxxxxxxx ;comb 对应的地址。这个函数需要两个整型参数,就通过堆栈来传
递:
;lresult=comb(2,3);
push 3
push 2
call xxxxxxxx
mov dword ptr [yyyyyyyy], eax ;yyyyyyyy 是长整型变量lresult 的地址
这里请注意两点。第一,在C 语言中,参数的压栈顺序是和参数顺序相反的,即后面
的参数先压栈,所以先执行push 3. 第二,在我们讨论的32 位系统中,如果不指明参数类
型,缺省的情况就是压入32 位双字。因此,两个push 指令总共压入了两个双字,即8 个
字节的数据。然后执行call 指令。call 指令又把返回地址,即下一条指令(mov dword
ptr....)的32 位地址压入,然后跳转到xxxxxxxx 去执行。
在comb 子程序入口处(xxxxxxxx),堆栈的状态是这样的:
03000000 (请回忆small endian 格式)
02000000
yyyyyyyy <--ESP 指向返回地址
前面讲过,子程序的标准起始代码是这样的:
push ebp ;保存原先的ebp
mov ebp, esp;建立框架指针
sub esp, XXX;给临时变量预留空间
.....
执行push ebp 之后,堆栈如下:
03000000
02000000
yyyyyyyy
old ebp <---- esp 指向原来的ebp
执行mov ebp,esp 之后,ebp 和esp 都指向原来的ebp. 然后sub esp, xxx 给临时变
量留空间。这里,只有一个临时变量temp,是一个长整数,需要4 个字节,所以xxx=4。
这样就建立了这个子程序的框架:
03000000
02000000
yyyyyyyy
old ebp <---- 当前ebp 指向这里
temp
所以子程序可以用[ebp 8]取得第一参数(m),用[ebp C]来取得第二参数(n),以此类推。
临时变量则都在ebp 下面,如这里的temp 就对应于[ebp-4].
子程序执行到最后,要返回temp 的值:
mov eax,[ebp-04]
然后执行相反的操作以撤销框架:
mov esp,ebp ;这时esp 和ebp 都指向old ebp,临时变量已经被撤销
pop ebp ;撤销框架指针,恢复原ebp.
这是esp 指向返回地址。紧接的retn 指令返回主程序:
retn 4
该指令从堆栈弹出返回地址装入EIP,从而返回到主程序去执行call 后面的指令。同时
调整esp(esp=esp 4*2),从而撤销参数,使堆栈恢复到调用子程序以前的状态,这就是堆栈
的平衡。调用子程序前后总是应该维持堆栈的平衡。从这里也可以看到,临时变量temp
已经随着子程序的返回而消失,所以试图返回一个指向临时变量的指针是非法的。
为了更好地支持高级语言,INTEL 还提供了指令Enter 和Leave 来自动完成框架的建
立和撤销。Enter 接受两个操作数,第一个指明给临时变量预留的字节数,第二个是子程
序嵌套调用层数,一般都为0。enter xxx,0 相当于:
push ebp
mov ebp,esp
sub esp,xxx
leave 则相当于:
mov esp,ebp
pop ebp
备注:忘记是在哪里收集的,没能注明原出处,若读者知道还请指出,谢谢