实验4 汇编应用编程和c语言程序反汇编分析
一、实验目的
1、理解80×25彩色字符模式显示原理
2、理解转移指令jmp,loop,jcxz的跳转原理,掌握使用其实现分支和循坏的用法
3、理解转移指令call,ret,retf的跳转原理,掌握组合使用call和ret/retf编写汇编子程序的方法,掌握参数传递方式
4、理解标志寄存器的作用
5、理解条件转移指令je,jz,ja,jg,jl等的跳转原理,掌握组合使用汇编指令cmp和条件转移指令实现分支和循环的用法
6、了解在visual stdio/Xcode等环境或利用gcc命令行参数反汇编C语言程序的方法,理解编译器生成的反汇编代码
7、综合应用寻址方式和汇编指令完成应用编程
二、实验准备
实验前,请复习/学习教材以下内容:
第9章 转移指令的原理
第10章 call和ret指令
第11章 标志寄存器
三、实验结论
1、实验任务1
教材[实验9 根据材料编程](P187-189)
编程:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串“welcome to masm!”。
在内存地址空间中,B8000H~BFFFFH供32KB的空间,为80×25彩色字符模式的显示缓冲区。该显示缓冲区分为8页,每页4KB(≈4000B),每页有25行,一行可显示80个字符,一个字符占两个字节,共160个字节,低位字节存储字符的ASCII码,高位字节存储字符的颜色属性。
屏幕中间对应的显存位置的计算:以B800H作为段地址,要在屏幕中间显示三行字符串,这三行字符串应分别显示在第12、13、14行。要显示的字符串占16个字节,每个字符的颜色属性占16个字节,共32个字节,因为一行有160个字节,所以要让字符显示在屏幕中间,应从每行的第33列,即第64个字节开始存放字符及其颜色属性。第12行的开始处的偏移地址为1760,再加上64,等于1824,转换为十六进制就是720H。所以要显示的第一行字符串的起始位置偏移地址应为720H,加上160个字节就是第二行字符串起始位置的偏移地址,加上320个字节就是第3行字符串的起始位置的偏移地址。
字符串颜色属性的设置:根据属性字节的格式即可按位设置属性字节
黑底绿字:属性字节为00000010B,十六进制为02H;
绿底红字:属性字节为00100100B,十六进制为24H;
白底蓝字:属性字节为01110001B,十六进制为71H。
1 assume cs:codesg 2 data segment 3 db 'welcome to masm!' 4 data ends 5 6 codesg segment 7 start: mov ax,data 8 mov ds,ax ;字符串的段地址 9 mov di,0 ;字符串的偏移地址 10 11 mov ax,0b800H 12 mov es,ax ;显示缓冲区的段地址 13 mov si,720H ;要显示的第一行字符串起始位置的偏移地址 14 15 mov cx,16 16 s: mov al,ds:[di] 17 mov es:[si],al ;设置第一行字符串的ASCII码 18 mov es:[si+160],al ;设置第二行字符串的ASCII码 19 mov es:[si+320],al ;设置第三行字符串的ASCII码 20 mov byte ptr es:[si+1],02H ;设置第一行字符串的颜色属性 21 mov byte ptr es:[si+160+1],24H ;设置第二行字符串的颜色属性 22 mov byte ptr es:[si+320+1],71H ;设置第三行字符串的颜色属性 23 inc di ;字符串的偏移地址加1 24 add si,2 ;显示缓冲区的偏移地址加2 25 loop s 26 27 mov ax,4c00H 28 int 21H 29 codesg ends 30 end start
将程序编译、连接后的执行结果如下:
2、实验任务2
编写子程序printStr,实现以指定颜色在屏幕上输出字符串。调用它,完成字符串输出。
1 assume cs:code, ds:data 2 data segment 3 str db 'try', 0 4 data ends 5 6 code segment 7 start: 8 mov ax, data 9 mov ds, ax ;设置入口参数ds 10 11 mov si, offset str ;设置字符串起始地址的偏移地址 12 mov al, 2 ;设置字符串颜色 13 call printStr 14 15 mov ah, 4ch 16 int 21h 17 18 printStr: 19 push bx 20 push cx 21 push si 22 push di 23 24 mov bx, 0b800H 25 mov es, bx 26 mov di, 0 27 s: mov cl, [si] 28 mov ch, 0 29 jcxz over 30 mov ch, al 31 mov es:[di], cx 32 inc si 33 add di, 2 34 jmp s 35 36 over:pop di 37 pop si 38 pop cx 39 pop bx 40 ret 41 42 code ends 43 end start
汇编、运行程序,观察运行结果:
对源程序做如下修改:
把line3改为:
1 str db 'another try', 0
把line12改为:
1 mov al,4
再次汇编、运行程序,观察运行结果:
基于运行结果,理解源代码,以及,组合使用转移指令call和ret实现子程序的原理与方法,在line18-40中:
line19-22,line36-39,这组对称使用的push、pop,这样用的目的是什么?
line19-22将bx、cx、si、di逐个入栈,line36-39使栈中的数据逐个出栈,并分别赋值给di、si、cx、bx,这样就可以让寄存器bx、cx、si、di恢复到原来的值。
line30的功能是什么?
寄存器cx的低位字节保存了字符的ASCII码,高位字节保存了字符的颜色属性,line30的功能就是将寄存器cx中有颜色的字符送到显存地址中。
3、实验任务3
使用任意文本编辑器,录入汇编程序task3.asm。
1 assume cs:code, ds:data 2 data segment 3 x dw 1984 4 str db 16 dup(0) 5 data ends 6 7 code segment 8 start: 9 mov ax, data 10 mov ds, ax ;数字字符串的段地址 11 mov ax, x ;要转换的整数 12 mov di, offset str ;数字字符串的起始地址的偏移地址 13 call num2str 14 15 mov ah, 4ch 16 int 21h 17 18 num2str: 19 push ax 20 push bx 21 push cx 22 push dx 23 24 mov cx, 0 25 mov bl, 10 26 s1: 27 div bl 28 inc cx 29 mov dl, ah 30 push dx 31 mov ah, 0 32 cmp al, 0 33 jne s1 34 s2: 35 pop dx 36 or dl, 30h 37 mov [di], dl 38 inc di 39 loop s2 40 41 pop dx 42 pop cx 43 pop bx 44 pop ax 45 46 ret 47 code ends 48 end start
阅读源代码,理解子程序num2str的汇编实现。
子任务1:
对task3.asm进行汇编、连接,得到可执行程序后,在debug中使用u命令反汇编,使用g命令执行到line15(程序退出之前),使用d命令查看数据段内容,观察是否把转换后的数字字符串“1984”存放在数据段中str标号后面的单元。
在debug中使用u命令进行反汇编,找到指令“mov ah,4ch”对应的cs:ip:076C:000E
用g命令执行到程序结束之前,然后用d命令查看数据段内容,发现转换后的数字字符串“1984”存放在数据段中str标号后面的单元:
子任务2:
对task3.asm源代码进行修改、完善,把task2.asm中用于输出以0结尾的字符串的子程序加进来,实现对转换后的字符串进行输出。
修改、完善后的代码:
1 assume cs:code, ds:data 2 data segment 3 x dw 1984 4 str db 16 dup(0) 5 data ends 6 7 code segment 8 start: 9 mov ax, data 10 mov ds, ax 11 mov ax, x 12 mov di, offset str 13 call num2str 14 mov si,offset str 15 call printStr 16 17 mov ah, 4ch 18 int 21h 19 20 num2str: 21 push ax 22 push bx 23 push cx 24 push dx 25 26 mov cx, 0 27 mov bl, 10 28 s1: 29 div bl 30 inc cx 31 mov dl, ah 32 push dx 33 mov ah, 0 34 cmp al, 0 35 jne s1 36 s2: 37 pop dx 38 or dl, 30h 39 mov [di], dl 40 inc di 41 loop s2 42 43 pop dx 44 pop cx 45 pop bx 46 pop ax 47 48 ret 49 50 printStr: 51 push bx 52 push cx 53 push si 54 push di 55 56 mov bx, 0b800H 57 mov es, bx 58 mov di, 0 59 s: mov cl, [si] 60 mov ch, 0 61 jcxz over 62 mov ch, 2 ;设置字符串颜色 63 mov es:[di], cx 64 inc si 65 add di, 2 66 jmp s 67 68 over: pop di 69 pop si 70 pop cx 71 pop bx 72 ret 73 code ends 74 end start
运行结果:
把task3.asm源代码中,line3中的整数改成0~2559之间的任意数值,运行测试,观察结果。
4、实验任务4
使用任意文本编辑器,录入汇编源程序task4.asm。
1 assume cs:code, ds:data 2 data segment 3 str db 80 dup(?) 4 data ends 5 6 code segment 7 start: 8 mov ax, data 9 mov ds, ax 10 mov si, 0 11 12 s1: 13 mov ah, 1 14 int 21h 15 mov [si], al 16 cmp al, '#' 17 je next 18 inc si 19 jmp s1 20 next: 21 mov cx, si 22 mov si, 0 23 s2: mov ah, 2 24 mov dl, [si] 25 int 21h 26 inc si 27 loop s2 28 29 mov ah, 4ch 30 int 21h 31 code ends 32 end start
汇编、连接、运行程序,输入一个字符串并以#结尾(比如,2020,bye#),观察运行结果。
结合运行结果,理解程序功能,了解软中断指令。具体地:
line12-19实现的功能是?
利用软中断的1号子功能,从键盘输入单个字符,并将字符保存到al中,通过cmp指令和je指令判断当前字符是不是#。如果当前字符是#,则指令“ cmp al,'#' "执行后zf=1,当zf=1时,满足je指令的转移条件;否则不满足转移条件,就将偏移地址si加1,继续从键盘输入字符。
line21-27实现的功能是?
利用软中断的2号子功能,在屏幕上输出单个字符。执行完s1的代码后,si是输入的最后一个字符的偏移地址,因为一个字符占一个字节,所以si的值对应输入字符的个数,将si赋给cx,得到输出单个字符的循环次数,然后再将si赋值0,作为第一个字符的偏移地址。line23-25就是利用2号子功能输出单个字符。然后偏移地址si加1,继续输出下一个字符。
5、实验任务5
在visual stdio集成环境中,编写一个简单的包含有函数调用的c程序。代码如下:
1 #include <stdio.h> 2 int sum(int, int); 3 int main() { 4 int a = 2, b = 7, c; 5 c = sum(a, b); 6 return 0; 7 } 8 9 int sum(int x, int y) { 10 return (x + y); 11 }
在line7,line13分别设置断点,在调试模式下,查看反汇编代码。
分析反汇编代码,从汇编的角度,观察高级语言中参数传递和返回值是通过什么实现的,以及参数入栈顺序,返回值的带回方式,等等。
主函数的反汇编代码:
1 #include <stdio.h> 2 int sum(int, int); 3 int main() { 4 001717B0 push ebp 5 001717B1 mov ebp,esp 6 001717B3 sub esp,0E4h 7 001717B9 push ebx 8 001717BA push esi 9 001717BB push edi 10 001717BC lea edi,[ebp-0E4h] 11 001717C2 mov ecx,39h 12 001717C7 mov eax,0CCCCCCCCh 13 001717CC rep stos dword ptr es:[edi] 14 001717CE mov ecx,offset _FD17926B_demo@cpp (017C003h) 15 001717D3 call @__CheckForDebuggerJustMyCode@4 (017130Ch) 16 int a = 2, b = 7, c; 17 001717D8 mov dword ptr [a],2 ;把2赋给a 18 001717DF mov dword ptr [b],7 ;把7赋给b 19 c = sum(a, b); 20 001717E6 mov eax,dword ptr [b] ;把b赋值给寄存器eax 21 001717E9 push eax ;eax入栈,即参数b的值先入栈 22 001717EA mov ecx,dword ptr [a] ;把a赋值给寄存器ecx 23 001717ED push ecx ;ecx入栈,即参数a的值入栈 24 001717EE call sum (017116Dh) ;通过call指令调用sum()函数 25 001717F3 add esp,8 26 001717F6 mov dword ptr [c],eax ;将寄存器eax的值赋给c 27 return 0; 28 001717F9 xor eax,eax ;通过异或使寄存器eax清零 29 } 30 001717FB pop edi 31 001717FC pop esi 32 001717FD pop ebx 33 001717FE add esp,0E4h 34 00171804 cmp ebp,esp 35 00171806 call __RTC_CheckEsp (0171235h) 36 0017180B mov esp,ebp 37 0017180D pop ebp 38 0017180E ret
sum()函数的反汇编代码:
1 int sum(int x, int y) { 2 00171740 push ebp 3 00171741 mov ebp,esp 4 00171743 sub esp,0C0h 5 00171749 push ebx 6 0017174A push esi 7 0017174B push edi 8 0017174C lea edi,[ebp-0C0h] 9 00171752 mov ecx,30h 10 00171757 mov eax,0CCCCCCCCh 11 0017175C rep stos dword ptr es:[edi] 12 0017175E mov ecx,offset _FD17926B_demo@cpp (017C003h) 13 00171763 call @__CheckForDebuggerJustMyCode@4 (017130Ch) 14 return (x + y); 15 00171768 mov eax,dword ptr [x] ;将参数x的值赋给寄存器eax 16 0017176B add eax,dword ptr [y] ;寄存器eax中的值与参数y的相加,并将和保存到eax中 17 } 18 0017176E pop edi 19 0017176F pop esi 20 00171770 pop ebx 21 00171771 add esp,0C0h 22 00171777 cmp ebp,esp 23 00171779 call __RTC_CheckEsp (0171235h) 24 0017177E mov esp,ebp 25 00171780 pop ebp 26 00171781 ret
参数b先入栈,然后参数a入栈,参数的入栈顺序是参数列表自右向左。在主程序中调用子程序是通过call指令实现的,子程序的返回值保存在一个寄存器中,子程序执行完毕后,主程序通过该寄存器将返回值赋给变量。