最近看完了“《汇编语言(第3版)》王爽著”,该书是基于DOS环境和TC2编译器讲的,老掉牙了是不是,不过看完了还是很有收获的.下面分析两段C语言程序生成的汇编指令,作为看完这本书后的总结.通过分析这两个程序可以明白函数是如果调用的,函数参数如果传递的,局部变量是怎么定义的等问题.
首先按照书中的介绍需要搭建环境,虽然现在几乎没人用DOS了,你可以用虚拟机搭建,或者你可以用DOSBOS搭建一个DOS环境.当然不建环境也没有关系,这不是重点.运行结果我会全部挂出来了,明白其中的道理就可以了.
先看第一个程序 demo1.c
#define Buffer ((char *)*(int far *)0x200)
int main(){
Buffer = (char *)malloc(20);
Buffer[10] = 0;
while(Buffer[10] != 8){
Buffer[Buffer[10]]='a'+Buffer[10];
Buffer[10]++;
}
free(Buffer);
}
复制代码
- 用TC2编译上面的代码,生成demo1.exe
debug demo1.exe
进入汇编调试环境. 执行r
可以查看各个寄存器的状态,如下图u
命令可以查看程序的汇编指令 因为可执行文件中除了包含自己编写的代码,还有它依赖的一些代码,所以要找到自己的代码部分,通过不断的执行u
命令查找,我发现main函数在内存偏移地址01FA处,u 01fa
命令可以查看该处的汇编代码
段地址:偏移地址 汇编指令 注解
;---main函数头部
076c:01fa push bp ;暂存bp
076c:01fb mov bp,sp ;栈顶指针寄存器sp的值赋给bp,访问压入到栈里的函数参数,以及定义局部变量都可以以bp为基准
;----以上函数开始部分为所以子程序的通用标准
076c:01fd mov ax,0014 ;设置寄存器ax值为0014,即十进制的20
076c:0200 push ax ;ax寄存器内容入栈,通过栈的方式传递malloc函数的参数,函数传参原来是这样实现的
076c:0201 call 04eb ;调用malloc函数(malloc函数的偏移地址是04eb)
076c:0204 pop cx ;malloc函数的参数出栈
;----完成调用malloc函数---
076c:0205 xor bx,bx ;bx寄存器异或操作即置0,任何数异或自身==0
076c:0207 mov es,bx ;段寄存器es置0,段寄存器不能直接设值,一般会通过ax,dx,bx等寄存器间接设置
076c:0209 mov bx,0200 ;设置寄存器bx值为0200
076c:020c es:
076c:020d mov [bx],ax ;把寄存器ax中的值放到es:[bx]指向的内存中,
;ax存放的是前面调用malloc函数的返回值,也就是分配的存储空间的地址我这里是0248,
;可见函数的返回值是放在ax寄存器中
;-----完成把malloc函数的返回值存储到内存0:0200位置
076c:020f xor bx,bx
076c:0211 mov es,bx
076c:0213 mov bx,0200
076c:0216 es:
076c:0217 mov bx,[bx]
076c:0219 mov byte ptr [bx+0a],00
;----完成把分配存储空间的第10个字节设为0
076c:021d jmp 025b ;cpu指针跳转到025b处,看看025b处是什么
076c:021f xor bx,bx
076c:0221 mov es,bx
076c:0223 mov bx,0200
076c:0226 es:
076c:0227 mov bx,[bx]
076c:0229 mov al,[bx+0a] ;把内存es:[bx+0a]的值存储到ax寄存器的低8位上面.该环境是16位的,ax总共16位,ah和al分别代表ax的高8位和低8位
;-------完成把分配存储空间的第10个字节的值存储到ax寄存器的低8位上
076c:022c add al,61 ;al寄存器加61,61是'a'的asc码
;--------完成'a'+Buffer[10]
076c:022e xor bx,bx
076c:0230 mov es,bx
076c:0232 mov bx,0200
076c:0235 es:
076c:0236 mov bx,[bx]
;------完成把分配存储空间的地址放到bx中
076c:0238 push ax ;暂存ax的值,此时ax值为('a'+Buffer[10])求的值,因为后面还会用到ax存储分配存储空间的第10个字节的值,值会被覆盖
076c:0239 push bx ;暂存bx的值,此时bx是分配存储空间的首地址
;------分配存储空间的第10个字节的值放到al中
076c:023a xor bx,bx
076c:023c mov es,bx
076c:0233 mov bx,0200
076c:0241 es:
076c:0242 mov bx,[bx]
076c:0244 mov al,[bx+0a]
076c:0247 cbw ;字节转换为字执行的操作:AL的内容符号扩展到AH,形成AX中的字。即如果(AL)的最高有效位为0,则(AH)=0;如(AL)的最高有效位为1,则(AH)=0FFH
076c:0248 pop bx ;取出栈顶元素给bx,该元素即即前面暂存的bx的值
076c:0249 add bx,ax ;bx加分配的存储空间第10个字节的值,对应Buffer[Buffer[10]]
076c:024b pop ax ;取出栈顶元素给ax,该元素即即前面暂存的ax的值
076c:024c mov [bx],al ;
;-----完成Buffer[Buffer[10]]='a'+Buffer[10];
076c:024e xor bx,bx
076c:0250 mov es,bx
076c:0252 mov bx,0200
076c:0255 es:
076c:0256 mov bx,[bx]
076c:0258 inc byte ptr [bx+0a]
;----完成分配的内存空间的第10个字节增1
076c:025b xor bx,bx
076c:025d mov es,bx
076c:025f mov bx,0200
076c:0262 es:
076c:0263 mov bx,[bx]
076c:0265 cmp byte ptr [bx+0a],08 ;比较分配空间的第10个字节的值与8的大小,这里会通过标志寄存器判断结果
076c:0269 jnz 021f ;如果上面的比较不相等,就让cpu指针跳转到021f
;-------完成判断表达式Buffer[10] != 8----
076c:026b xor bx,bx
076c:026d mov es,bx
076c:026f mov bx,0200
076c:0272 es:
076c:0273 push [bx]
076c:0275 call 06eb ;调用free函数
076c:0278 pop cx ;free函数参数出栈
;------完成调用free函数-----
;---power函数结尾---
076c:0279 pop bp
076c:027a ret
;---main函数结束
复制代码
通过上面代码的分析可以看到0:0x200处存储的是molloc函数分配的空间的地址,而这段20个字节长度的存储空间存储的是一个字符串,该程序运行完后该存储空间会有"abcdefgh"8个字符.可以用t
命令一步一步的查看程序的执行.最好不用g命令,有时候g
命令跨步大了会与预期不符.
下面再分析一段代码看看函数是如何传参和调用的
demo2.c
#include <stdio.h>
int power(int a, int x);
int main(void) {
int a = 2;
int x = 3;
int y = power(a, x);
printf("get the result %d^%d=%d\n", a, x, y);
}
int power(int a, int x) {
int i;
int res = 1;
for(i = 0; i < x; i++)
res *= a;
return res;
}
复制代码
编译生成的汇编指令如下
段地址:偏移地址 汇编指令 注解
;---main函数头部
076c:01fa push bp ;暂存bp
076c:01fb mov bp,sp ;用bp记录当下的sp
076c:01fd sub sp,02 ;分配局部变量栈空间
076c:0200 push si ;暂存用到的寄存器
076c:0201 push di ;暂存用到的寄存器
;----以上函数开始部分为所以子程序的通用标准
076c:0202 mov si,0002
076c:0205 mov di,0003
076c:0208 push di
076c:0209 push si ;power函数栈传参
076c:020a call 0227 ;调用power函数.
;call后面的地址是通过被调函数地址与下一条指令的地址相对偏移量计算的,
;而与其相对的jump loop jcxz等转移指令后面的地址是绝对地址
;执行call指令进行如下两步
;1. 将当前的ip压入栈中,相当于push ip
;2. 转移,相当于jmp near ptr 标号
076c:020d pop cx
076c:020e pop cx ;调用两次pop cx ,是对前面调用power入栈的参数做出栈处理
;---函数power调用结束
076c:020f mov [bp-02],ax ;ax存储函数power返回值,[bp-02]就是y这个局部变量,局部变量要么在栈中,要么在寄存器中
076c:0212 push [bp-02]
076c:0215 push di
076c:0216 push si
076c:0217 mov ax,0194 ;通过命令d 0194 01ab,可以看到0194是存储字符串"get the result %d^%d=%d\n"的地址,0194 01ab分别是存储该字符串的开始和结束地址
076c:021a push ax ;printf函数栈传参
076c:021b call 0afc ;调用printf函数
076c:021e add sp,08 ;对调用pringf的参数做出栈处理,一共是4次push,8个字节
;---函数printf调用结束
;---power函数结尾---
076c:0221 pop di ;恢复di
076c:0222 pop si ;恢复si
076c:0223 mov sp,bp ;局部变量退栈
076c:0225 pop bp ;恢复bp
076c:0226 ret
;执行ret指令进行如下:
;栈顶值出栈给ip,相当于 pop ip
;----main函数结束
;--power函数开始
076c:0227 push bp ;暂存 bp
076c:0228 mov bp,sp ;用bp记录当下的sp
;访问压入到栈里的函数参数,以及定义局部变量都以bp为基准,可以知道参数地址比bp大,变量地址比bp小
076c:022a push si ;暂存 si
076c:022b push di ;暂存 di
;----以上函数开始部分为所以子程序的通用标准
076c:022c mov si,0001 ;这里的si就是局部变量res,局部变量要么在栈中,要么在寄存器中
076c:022f xor di,di ;用id做递增计数
076c:0231 jmp 023b ;跳转到023b
076c:0233 mov ax,si
076c:0235 mul word ptr [bp+04] ;mul是乘法指令,[bp+04]存储的值跟ax存储的值相乘,结果存储到ax中,
;ss:[bp+06]是函数的第二个参数x,ss:[bp+04]是函数的第一个参数a,ss:[bp+02]是0227处入栈的bp,ss:[bp]是调用power函数前入栈的cpu指针地址
076c:0238 mov si,ax ;赋值si
076c:023a inc di ;di增1
076c:023b cmp di,[bp+6] ;di寄存器的值与[bp+6]栈内存处的值比较
076c:023e jl 0233 ;若di寄存器的值小于[bp+6]栈内存处的值,cpu指针移动到0233处,否则指针移动到下面的指令地址
076c:0240 mov ax,si ;返回值放到ax中
076c:0242 jmp 0244
;---power函数结尾---
076c:0244 pop di ;恢复di
076c:0245 pop si ;恢复si
076c:0246 pop bp ;恢复bp
076c:0247 ret
;----power函数结束
复制代码
通过上面的分析可知,函数的参数是通过栈传递的,局部变量会存储在栈或者寄存器里面.执行完函数的时候参数和局部变量都会出栈.
现在改一下demo2.c,把a改成long类型,如下
#include <stdio.h>
long power(long a, int x);
int main(void) {
long a = 250;
int x = 2;
long y = power(a, x);
printf("get the result %ld^%d=%ld\n", a, x, y);
}
long power(long a, int x) {
int i;
long res = 1;
for(i = 0; i < x; i++)
res *= a;
return res;
}
复制代码
生成汇编代码如下
段地址:偏移地址 汇编指令 注解
;函数头
076c:01fa push bp ;暂存bp
076c:01fb mov bp,sp ;用bp记录当下的sp
076c:01fd sub sp,8 ;分配局部变量栈空间
076c:0200 push si ;暂存用到的寄存器
076c:0201 mov word ptr [bp-06],0000 ;这个不知道做什么用的
076c:0206 mov word ptr [bp-08],00fa ;定义变量a
076c:020b mov si,0002 ;定义变量x
;开始调用power
;开始传参
076c:020e push si
076c:020f push [bp-06]
076c:0212 push [bp-08]
;完成传参
076c:0215 call 023d ;调用子程序
076c:0218 add sp,06 ;参数退栈
;完成调用power
;返回值,long类型一个寄存器放不下,dx存储高16位, ax存储低16位
076c:021b mov [bp-02],dx
076c:021e mov [bp-04],ax
076c:0221 push [bp-02]
076c:0224 push [bp-04]
076c:0227 push si
076c:0228 push [bp-06]
076c:022b push [bp-08]
076c:022e mov ax,0194
076c:0231 push ax
076c:0232 call 0b49
076c:0235 add sp,0c
;完成调用printf
;main函数尾
076c:0238 pop si ;恢复用到的寄存器
076c:0239 mov sp,bp ;局部变量退栈
076c:023b pop bp ;恢复bp
076c:023c ret ;子程序返回
;power函数头
076c:023d push bp ;暂存bp
076c:023e mov bp,sp ;用bp记录当下的sp
076c:0240 sub sp, 04 ;分配局部变量栈空间
076c:0243 push si ;暂存用到的寄存器
076c:0244 mov word ptr [bp-02],0000
076c:0249 mov word ptr [bp-04],0001 ;定义变量res
076c:024e xor si,si ;用寄存器si定义变量i
076c:0250 jmp 026a
;调用大整数相乘的子程序,long类型超过了16位所以不能直接用mul
076c:0252 mov dx,[bp-02]
076c:0255 mov ax,[bp-04] ;变量res
076c:0258 mov cx,[bp+06] ;0000
076c:025b mov bx,[bp+4] ;参数a
076c:025e call 076c:0a91
076c:0263 mov [bp-02],dx
076c:0266 mov [bp-04],ax ;相乘的结果存储到res中
076c:0269 inc si ;变量i增1
076c:026a cmp si,[bp+08] ;比较i与参数x
076c:026d jl 0252 ;小于则跳转到0252
076c:026f mov dx,[bp-02]
076c:0272 mov ax,[bp-04] ;返回值高16位放到dx中,低16位放到ax中
076c:0275 jmp 0277
;power函数尾
076c:0277 pop si ;恢复si
076c:0278 mov sp,bp ;局部变量退栈
076c:027a pop bp ;恢复bp
076c:027b ret ;子程序返回
复制代码
在该环境下long类型是32位,16位的寄存器已经放不下一个long类型的数据,所以用两个寄存器或4个字节大小的内存存储.
总结一下编译器如何处理对函数的调用以及传参,如何处理局部变量的定义,如何读取参数
- 函数的调用
;开始传参
push 寄存器或内存里的值
...
n个参数,就push n次
;完成传参
call fun ;调用子程序
;add sp,n ;参数退栈
;完成调用power
复制代码
- 函数局部变量定义,参数读取
;函数头
push bp ;暂存bp
mov bp,sp ;用bp记录当下的sp
;访问压入到栈里的函数参数,以及定义局部变量都以bp为基准,可以知道参数地址比bp大,变量地址比bp小
sub sp, n ;分配局部变量栈空间
push 寄存器 ;暂存用到的寄存器
;函数体
mov word ptr [bp-x],value ;定义变量
...
其他功能指令
ax存储返回值,或者如果返回值高于16位,用dx存储返回值的高16位,ax存储返回值的低16位
;函数尾
pop 寄存器 ;恢复用到的寄存器
mov sp,bp ;局部变量退栈
pop bp ;恢复bp
ret ;子程序返回
复制代码
附:
- 调试指令
r 查看寄存器
u[+地址] 以汇编码的形式列出 d[+地址] 以16进制码的形式列出
t 单步执行指令
g+地址 执行到指定地址
p 执行完子程序
a[+地址] 以汇编码的形式修改
e[+地址 | 00 00 00 ...16进制码] 以16进制的形式修改
- 寄存器使用
cs ss ds es 段地址寄存器,存储内存段地址
ax bx cx dx 通用寄存器,存放普通数据
bp si di bx 偏移地址寄存器
寄存器内存访问: 段地址*16+偏移地址=物理地址
常用组合举例:
ss:sp ss栈段地址寄存器,sp栈偏移地址寄存器
cs:ip cs cpu指针段地址寄存器,ip cpu指针偏移地址寄存器
ds:[bx] ds[任何偏移地址寄存器]组合
es:[di] es[任何偏移地址寄存器]组合
[bp] bp默认段寄存器是ss,其他偏移地址寄存器默认的段寄存器是ds
段寄存器不能直接赋值,可以通过通用寄存器间接赋值