分析一段C程序编译后生成的汇编指令

最近看完了“《汇编语言(第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);
}
复制代码
  1. 用TC2编译上面的代码,生成demo1.exe
  2. 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个字节大小的内存存储.

总结一下编译器如何处理对函数的调用以及传参,如何处理局部变量的定义,如何读取参数

  1. 函数的调用
;开始传参
push 寄存器或内存里的值
...
n个参数,就push n次
;完成传参
call fun ;调用子程序
;add sp,n ;参数退栈
;完成调用power
复制代码
  1. 函数局部变量定义,参数读取
;函数头
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 ;子程序返回
复制代码

附:

  1. 调试指令

r 查看寄存器
u[+地址] 以汇编码的形式列出 d[+地址] 以16进制码的形式列出
t 单步执行指令
g+地址 执行到指定地址
p 执行完子程序
a[+地址] 以汇编码的形式修改
e[+地址 | 00 00 00 ...16进制码] 以16进制的形式修改

  1. 寄存器使用

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

段寄存器不能直接赋值,可以通过通用寄存器间接赋值

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值