汇编语言程序设计 实验4 汇编应用编程和c语言程序反汇编分析

汇编语言程序设计 实验4 汇编应用编程和c语言程序反汇编分析 

一、实验目的

1. 理解80×25彩色字符模式显示原理
2. 理解转移指令jmp, loop, jcxz的跳转原理,掌握使用其实现分支和循环的用法
3. 理解转移指令call, ret, retf的跳转原理,掌握组合使用callret/retf编写汇编子程序的方法,掌握参数传递方式
4. 理解标志寄存器的作用
5. 理解条件转移指令je, jz, ja, jb, jg, jl等的跳转原理,掌握组合使用汇编指令cmp和条件转移指令实现分支和循环的用法
6. 了解在visual studio/Xcode等环境或利用gcc命令行参数反汇编c语言程序的方法,理解编译器生成的反汇编代码
7. 综合应用寻址方式和汇编指令完成应用编程

二、实验内容及结果

(一)实验任务一

教材「实验9 根据材料编程」(P187-189)
编程:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串'welcome to masm!'
。 
源代码如下:

assume cs:code

data segment
	db 'welcome to masm!'  ;要显示的内容
data ends

code segment
	start:						;设置数据段地址
			mov ax,data
			mov ds,ax
			
			mov ax,0B800H		;B800H显示缓冲区的起始地址
			mov es,ax
			
			mov bx,0			;字符串内容的偏移地址,每次增加1

			mov bp,64           ;缓冲区的偏移地址,每次增加2
			mov cx,16           ;循环16次
			a:	mov al,ds:[bx]			;取得字符串的字节数据
				mov ah,02H				;黑底绿色
				mov es:[bp+6E0H],ax		;(垂直居中)显示在第11行上
				mov ah,24H				;绿底红色
				mov es:[bp+780H],ax		;(垂直居中)显示在第12行上
				mov ah,71H				;白底蓝色
				mov es:[bp+820H],ax		;(垂直居中)显示在第13行上

				inc bx                  ;累加操作
				add bp,2
			loop a
			
			mov ax,4C00H
			int 21H
code ends
end start

使用masmlink对exp9.asm进行汇编、链接,得到可执行文件exp9.exe,运行并观察结果。

1、具体的汇编、链接过程可以参考《汇编语言程序设计 实验2 汇编源程序编写与汇编、调试》。运行exp9.exe文件,得到结果:

(二)实验任务二

编写子程序printStr,实现以指定颜色在屏幕上输出字符串。调用它,完成字符串输出。
使用任意文本编辑器,录入汇编源程序task2.asm,源代码如下:

assume cs:code, ds:data
data segment
    str db 'try', 0
data ends

code segment
start:  
    mov ax, data
    mov ds, ax

    mov si, offset str
    mov al, 2
    call printStr

    mov ah, 4ch
    int 21h

printStr:
    push bx
    push cx
    push si
    push di

    mov bx, 0b800H
    mov es, bx
    mov di, 0
s:      mov cl, [si]
    mov ch, 0
    jcxz over
    mov ch, al
    mov es:[di], cx
    inc si
    add di, 2
    jmp s

over:   pop di
    pop si
    pop cx
    pop bx
    ret

code ends
end start

使用masmlink对task2.asm进行汇编、链接,得到可执行文件task2.exe,运行结果如下所示:

 对源程序做如下修改:
line3改为:

str db 'another try', 0

修改后的结果如下所示:

line12改为:  

mov al, 4

修改后的结果如下所示:

 可以得知:mov al改变了字符的颜色属性

 基于运行结果,理解源代码,以及,组合使用转移指令callret实现子程序的原理与方法。具体地,在line18-40中:

line19-22, line36-39,这组对称使用的pushpop,这样用的目的是:利于恢复各寄存器的值。 

line30的功能是:通过“mov es:[di], cx”可以读取目标数据存入es。

(三)实验任务三

使用任意文本编辑器,录入汇编源程序task3.asm,源代码如下所示:

assume cs:code, ds:data
data segment
        x dw 1984
        str db 16 dup(0)
data ends

code segment
start:  
        mov ax, data
        mov ds, ax
        mov ax, x
        mov di, offset str
        call num2str

        mov ah, 4ch
        int 21h

num2str:
        push ax
        push bx
        push cx
        push dx
        
        mov cx, 0
        mov bl, 10
s1:      
        div bl
        inc cx
        mov dl, ah
        push dx
        mov ah, 0
        cmp al, 0
        jne s1
s2:        
        pop dx
        or dl, 30h
        mov [di], dl
        inc di
        loop s2
        
        pop dx
        pop cx
        pop bx
        pop ax

        ret
code ends
end start

首先使用u命令对task.exe进行反汇编,结果如下所示:

 使用g命令执行到line15(程序退出之前)

-g 0e

 使用d命令查看数据段内容 ,结果如下所示:

 

task3.asm源代码进行修改、完善,把task2.asm中用于输出以0结尾的字符串的子程序加进来,实现对转换后的字符串进行输出。  源代码如下所示:

assume cs:code, ds:data
data segment
        x dw 1984
        str db 16 dup(0)
data ends

code segment
start:
        mov ax, data
        mov ds, ax
        mov ax, x
        mov di, offset str
        call num2str
        mov si, offset str
        call printStr
        mov ah, 4ch
        int 21h

num2str:
        push ax
        push bx
        push cx
        push dx
        mov cx, 0
        mov bl, 10
s1:
        div bl
        inc cx
        mov dl, ah
        push dx
        mov ah, 0
        cmp al, 0
        jne s1
s2:
        pop dx
        or dl, 30h
        mov [di], dl
        inc di
        loop s2
        pop dx
        pop cx
        pop bx
        pop ax
        ret

printStr:
    push bx
    push cx
    push si
    push di
    mov bx, 0b800H
    mov es, bx
    mov di, 0
s:
    mov cl, [si]
    mov ch, 0
    jcxz over
    mov ch, al
    mov es:[di], cx
    inc si
    add di, 2
    jmp s

over:   pop di
    pop si
    pop cx
    pop bx
    ret
code ends
end start

结果如下所示:

 

task3.asm源代码中,line3中整数改成0~2559之间的任意数值,运行测试,结果如下所示:

 

(四)实验任务四

使用任意文本编辑器,录入汇编源程序task4.asm,源代码如下所示:

assume cs:code, ds:data
data segment
        str db 80 dup(?)
data ends

code segment
start:  
        mov ax, data
        mov ds, ax
        mov si, 0

s1:        
        mov ah, 1
        int 21h
        mov [si], al
        cmp al, '#'
        je next
        inc si
        jmp s1
next:
        mov cx, si
        mov si, 0
s2:     mov ah, 2
        mov dl, [si]
        int 21h
        inc si
        loop s2

        mov ah, 4ch
        int 21h
code ends
end start

运行结果如下所示:

 line12-19实现的功能是:含有中断转移指令和条件跳转指令,判断输入的字符是否为#,并将不是#的字符存入栈中

line21-27实现的功能是:含有循环指令,输出存在栈内的字符

(五)实验任务五

visual studio集成环境中,编写一个简单的包含有函数调用的c程序。代码如下: 

#include <stdio.h>
int sum(int, int);

int main() {
	int a = 2, b = 7, c;

	c = sum(a, b);

	return 0;
} 

int sum(int x, int y) {
	return (x + y);
}

在line7和line13处添加断点,如图所示:

 在调试状态下(快捷键F5)点击“调试”->“窗口”->“反汇编”,即查看反汇编代码,结果如下:

结果如下所示:

int sum(int x, int y) {
00211740  push        ebp  
00211741  mov         ebp,esp  
00211743  sub         esp,0C0h  
00211749  push        ebx  
0021174A  push        esi  
0021174B  push        edi  
0021174C  lea         edi,[ebp-0C0h]  
00211752  mov         ecx,30h  
00211757  mov         eax,0CCCCCCCCh  
0021175C  rep stos    dword ptr es:[edi]  
0021175E  mov         ecx,offset _7FF23424_test@cpp (021C003h)  
00211763  call        @__CheckForDebuggerJustMyCode@4 (021130Ch)  
    return (x + y);
00211768  mov         eax,dword ptr [x]  
0021176B  add         eax,dword ptr [y]  
}
0021176E  pop         edi  
0021176F  pop         esi  
00211770  pop         ebx  
00211771  add         esp,0C0h  
00211777  cmp         ebp,esp  
00211779  call        __RTC_CheckEsp (0211235h)  
0021177E  mov         esp,ebp  
00211780  pop         ebp  
00211781  ret  

 反汇编代码分析:(红色为c语言程序蓝色为汇编语言程序,黑色为分析)

int main() {
002117B0 push ebp   ;保存ebp,此时EBP =005BFA38,ESP = 005BFA18

002117B1 mov ebp,esp    ;将esp放入ebp中,此时ebp和esp相同,即执行后ESP = 005BFA18 ,EBP = 005BFA18。原EBP值已经被压栈(位于栈顶),而新的EBP又恰恰指向栈顶。

002117B3 sub esp,0E4h     ;把esp往上移动一个范围,等于在栈中空出一片空间来存局部变量,执行这句后ESP = 005BF934, EBP = 005BFA18,EBX = 00745000

002117B9 push ebx 
002117BA push esi
002117BB push edi      ;保存三个寄存器的值
002117BC lea edi,[ebp-0E4h]       ;把ebp-oE4h加载到edi中,保存局部变量的区域
002117C2 mov ecx,39h
002117C7 mov eax,0CCCCCCCCh
002117CC rep stos dword ptr es:[edi]       ;从ebp-0E4h开始的区域初始化成全部0CCCCCCCCh,初始化局部变量空间

int a = 2, b = 7, c;

002117D8  mov         dword ptr [a],2     ;把2赋给a

002117DF  mov         dword ptr [b],7     ;把7赋给b

 c = sum(a, b);

00FD17E6 mov eax,dword ptr [b]     ;把b的值赋给eax
00FD17E9 push eax     ;eax入栈
00FD17EA mov ecx,dword ptr [a]     ;把a的值赋给ecx
00FD17ED push ecx    ;ecx入栈
00FD17EE call sum (0FD116Dh)     ;调用sum函数,可以按F11跟进
00FD17F3 add esp,8     ;调用完函数后恢复/释放栈,执行后ESP = 0079F75C,与sum函数的参数入栈前的数值一致

00FD17F6 mov dword ptr [c],eax     ;将结果存放在result中

return 0;
00FD17F9 xor eax,eax     ;异或后寄存器清零
}
00FD17FB pop edi     
00FD17FC pop esi
00FD17FD pop ebx     ;恢复原来寄存器的值
00FD17FE add esp,0E4h     ;恢复ESP,对应之前的sub esp,0E4h
00FD1804 cmp ebp,esp     ;检查esp是否正常,不正常就进入下边的call里面debug
00FD1806 call __RTC_CheckEsp (0FD1235h)     ;处理可能出现的堆栈异常,如果有的话,就会陷入debug
00FD180B mov esp,ebp 
00FD180D pop ebp    ;恢复原来的esp和ebp,让上一个调用函数正常使用
00FD180E ret     ;将返回地址存入eip,转移流程

以上是主函数调用的反汇编过程,以下部分是调用sum函数的过程:

int sum(int x, int y) {
00FD1740 push ebp     
00FD1741 mov ebp,esp
00FD1743 sub esp,0C0h
00FD1749 push ebx
00FD174A push esi
00FD174B push edi
00FD174C lea edi,[ebp-0C0h]
00FD1752 mov ecx,30h
00FD1757 mov eax,0CCCCCCCCh
00FD175C rep stos dword ptr es:[edi]
00FD175E mov ecx,offset _7FF23424_test@cpp (0FDC003h)
00FD1763 call @__CheckForDebuggerJustMyCode@4 (0FD130Ch)     ;与主函数的调用过程几乎相似

return (x + y);
00FD1768 mov eax,dword ptr [x]     ;取第一个参数放在eax
00FD176B add eax,dword ptr [y]     ;取第二个参数,与eax中的数值相加并存在eax中
}
00FD176E pop edi
00FD176F pop esi
00FD1770 pop ebx
00FD1771 add esp,0C0h
00FD1777 cmp ebp,esp
00FD1779 call __RTC_CheckEsp (0FD1235h)
00FD177E mov esp,ebp
00FD1780 pop ebp
00FD1781 ret     ;与主函数的调用过程几乎相似

三、实验总结

函数调用过程主要由参数传递、地址跳转、局部变量分配和赋初值、执行函数体,结果返回等几个步骤组成。
参数传递及函数跳转
参数由实参传递给形参。在底层实现上,即是实参按照函数调用规定压入堆栈。参数传递完成后就通过CALL指令由当前程序跳转到子程序处。
局部变量分配并赋值
函 数的“{”被认为是分配局部变量空间的时机。在汇编层面局部变量分配体现为堆栈中以 EBP 寄存器为基址向低地址端分配的一个连续区域,通过 EBP 寄存器的相对寻址方式来寻址函数内的局部变量。由于堆栈增长的方向是高地址端到低地址端,因此函数中先定义的局部变量地址较大,后定义的变量地址逐渐变小,相邻定义的变量其地址一定相邻[2]。由于全局数据和局部数据定义在不用的数据区而并不与局部变量相邻,根据程序局部性原理,相邻的数据会被缓存,因此对相同的运算,局部变量作为操作数的运算效率就可能高于有全局变量参与的运算。同时,局部变量分配和回收只需要移动堆栈指针ESP,因此效率最高。
寻址函数的参数
参数存放在以 EBP 为基址的高地址端。对参数的访问同样是通过EBP 寄存器相对寻址操作来实现。
执行函数体内的语句
函数内和具体功能相关的语句被转化成一系列汇编语句。
返回值
return 语句将返回值返回到主调函数。在底层,参数是通过 EAX 寄存器或 EDX 寄存器传递给主调函数。
返回主调函数
函数的“}”被解释为函数体已经执行完。遇到“}”时,会将堆栈中的局部变量、程序中压入堆栈的寄存器的值全部弹出,将之前 CALL指令执行时压入堆栈的函数返回地址弹到指令指针寄存器 EIP,从而返回到主调函数。

posted @ 2020-12-11 18:35  小徐小徐嘿呀  阅读(630)  评论(1)    收藏  举报