程序基础
- 三大结构
- 顺序结构
- 选择结构
- 循环结构
- 函数
选择结构
- if … else if … else
int nDay = 0;
scanf("%d", &nDay);
if( nDay == 1){
printf("星期一");
}else if(nDay==2){
printf("星期二");
}else if(nDay==3){
printf("星期三");
}
汇编版本:
fun proc
push ebp
mov ebp,esp
sub esp , 4 ; 为局部变量分配栈空间.
; ebp - 4 ==> nDay的位置
lea eax , [ebp-4]; eax= &nDay
push eax
push "%d"
call crt_scanf ; scanf("%d",&nDay)
cmp dword ptr [ebp-4] , 1
; if(nDay == 1)
jne _IF_1
push "星期一"
call crt_printf
add esp , 4
jmp _ENDIF
_IF_1:
cmp dword ptr [ebp-4] , 2
jne _IF_2
push "星期二"
call crt_printf
add esp , 4
jmp _ENDIF
_IF_2:
cmp dword ptr [ebp-4] , 3
jne _IF_3
push "星期三"
call crt_printf
add esp , 4
jmp _ENDIF
_IF_3:
_ENDIF:
mov esp, ebp
pop ebp
fun endp
总结
- 在C语言中, 使用
{}
来分隔每个语句块 - 在汇编指令是自上而下逐条执行, 没有跳转指令是不会跑到其它位置执行代码的.
在汇编构建一个语句块的方式是:
- 在语句块前加入一条条件跳转指令
- 在语句块后加入一条无条件跳转指令
_IF_2:
cmp dword ptr [ebp-4] , 3 ;语句块
jne _IF_3 ;的开始
push "星期三"
call crt_printf
add esp , 4
jmp _ENDIF ;语句块的结束
switch … case ```c int nDay=0; scanf(“%d”, &nDay);
switch( nDay ){ case 1: printf(“星期一”);break; case 2: printf(“星期二”);break; case 3: printf(“星期三”);break; }
<br />汇编版本:
1. 和if的版本一样.
2. 使用跳转表
```assembly
.386
.model flat,stdcall
option casemap:none
include msvcrt.inc
includelib msvcrt.lib
.const
pszD db "%d",0
pszTips1 db "星期一",0dh,0ah,0
pszTips2 db "星期二",0dh,0ah,0
pszTips3 db "星期三",0dh,0ah,0
.code
fun proc
push ebp
mov ebp,esp
sub esp , 4 ; 为局部变量分配栈空间.
; ebp - 4 ==> nDay的位置
lea eax , [ebp-4]; eax= &nDay
push eax
push offset pszD
call crt_scanf ; scanf("%d",&nDay)
cmp dword ptr [ebp-4] , 3
jg _ENDIF
; nday-=1
;jmp jmptab[nDay];
mov eax , [ebp-4]; eax = nDay
dec eax ; eax = nDay - 1
; 通过使用输入的值作为下标
; 从数组中索引出一个地址. 然后再跳转
; 到此地址上.
jmp dword ptr [jmptab + eax * 4]
; 在代码中定义了数据.
; DwORD jmptab[3]={ _IF_0,_IF_1,_IF_2};
; 编译之后, _IF_0标签就会编译成代码的地址
jmptab dd _IF_0,_IF_1,_IF_2
_IF_0:
; if(nDay == 1)``
push offset pszTips1
call crt_printf
add esp , 4
jmp _ENDIF
_IF_1:
push offset pszTips2
call crt_printf
add esp , 4
jmp _ENDIF
_IF_2:
push offset pszTips3
call crt_printf
add esp , 4
jmp _ENDIF
_ENDIF:
mov esp, ebp
pop ebp
ret
fun endp
main:
call fun
ret
end main
循环结构
- while
int i=0;
while( i < 100) {
printf("%d\n",i);
++i;
}
汇编版本:
通过条件跳转指令来模拟
fun proc
; 因为没有形参, 所以可以不需要开辟栈帧.
; 直接使用esp来索引局部变量.
sub esp , 4; 局部变量位置 => [esp]
mov [esp], 0 ; i=0;
;while( i < 100) {
_WHILE:
cmp dword ptr[esp] , 100
jg _ENDWHILE ; 如果大于就结束循环
;printf("%d\n",i);
push dword ptr[esp]
push "%d\n"
call crt_printf
add esp , 8
;++i
inc dword ptr [esp]
jmp _WHILE
_ENDWHILE:
add esp , 4
ret
fun endp;
do…while
int i=0;
do {
printf("%d\n",i);
++i;
}while( i < 100) ;
fun_dowhile proc
sub esp , 4 ; int i;
mov dword ptr [esp] , 0
_DOWHILE:
push dword ptr [esp];
push "%d\n"
call crt_printf
add esp , 8
inc dword ptr [esp];
cmp dword ptr [esp] , 100
jl _DOWHILE
fun_dowhile endp
for
for( int i = 0; i<100;++i){
printf("%d\n",i);
}
for循环的结构和while循环一样.
fun proc
; 因为没有形参, 所以可以不需要开辟栈帧.
; 直接使用esp来索引局部变量.
sub esp , 4; 局部变量位置 => [esp]
mov [esp], 0 ; i=0;
;while( i < 100) {
_WHILE:
cmp dword ptr[esp] , 100
jg _ENDWHILE ; 如果大于就结束循环
;printf("%d\n",i);
push dword ptr[esp]
push "%d\n"
call crt_printf
add esp , 8
;++i
inc dword ptr [esp]
jmp _WHILE
_ENDWHILE:
add esp , 4
ret
fun endp;
函数
调用约定
调用预定 | 传参方式 | 平衡栈方式 |
---|---|---|
cdecl(C调用约定) | 从右往左 | 在函数外平衡(调用者平衡),通过add esp,xx的方式平衡 |
stdcall标准调用约定 | 从右往左 | 在函数内平衡(被调用者平衡),通过ret xx的方式平衡. |
fastcall(快速调用约定) | 前两个参依次使用ecx,edx来传递, 后续函数通过栈传递(从右往左依次入栈) | 在函数内平衡(被调用者平衡),通过ret xx的方式平衡. |
thiscall(对象调用约定) | 通过ecx来保存this指针.参数从右往左依次入栈 | 在函数内平衡(被调用者平衡),通过ret xx的方式平衡. |
总结
- 调用约定了制约在代码中传参的方式和平衡栈的方式
- 在汇编中调用函数前需要搞清楚, 这个函数使用什么调用约定, 否则参数就无法正确传递.
函数调用总结
- 参数是通过栈来传递的. 有时候是直接push到栈中, 有时候也可以先sub esp拉高栈顶, 再使用mov [esp]的方式将实参赋值到栈中, 总之能够将实参放入到栈中合适的位置就可以了.
- esp栈顶高地址的位置一般都是被使用过的, 不应该随意修改. 而esp低地址的位置一般是没有被使用.可以用来保存新的数据.
- 函数调用完毕之后, 需要保证栈的平衡.
- 函数的返回值一般是约定使用eax来保存.
函数栈帧
实参通过栈传递之后, 在函数内部怎么定位? ```assembly ; 定义一个函数 ; void fun(int n1, int n2, int n3) fun proc push ebp ; 因为要使用ebp来保存当前栈顶的值, 因此需要先将寄存器的值备份起来. mov ebp, esp; 将栈顶位置保存到ebp中, 这样即使栈顶发生了浮动, 也不会影响通过ebp+固定偏移的方式去定位到栈内的形参了.
; 离开函数前, 需要恢复在函数内部中使用寄存器的值. ; 恢复esp和ebp的值. mov esp , ebp; 因为在函数开头,将esp的值保存到了ebp中, 而ebp在整个函数中是不会被修改的, 所以,esp的值就可以使用ebp来恢复. pop ebp ; 因为在函数开头, ebp的值被push中保存了, 因此, 可以直接从栈中pop出来恢复ebp的值. ret 0ch fun endp
push 3 push 2 push 1 call fun ```
使用局部变量
- 在打开栈帧之后, 需要给局部变量开辟栈空间
- 通过sub esp, 所有局部变量的字节数
- esp低地址的位置一般是没有被使用.可以用来保存新的数据, 所以, 没有将栈顶往上抬, 栈空间内的数据就可能被push或者call指令的操作所覆盖
- 通过
ebp-4
得到第一个局部变量的偏移
易失性寄存器
一般进入函数之后, 函数内部使用寄存器前需要备份寄存器的值, 离开函数时, 会将寄存器的值恢复回去, 但有些寄存器不会被恢复, 这些不会被恢复的寄存器被成为易失性寄存器
- windows的API的易失性寄存器是eax
- C运行时库函数(crt_xxx系列函数)的易失性寄存器是eax,ecx,edx,ebx