程序的启动
Win32应用程序都需要实现WinMain函数
Windows程序的执行并非从WinMain开始,而是从编译器生成的一段代码开始
所有C/C++程序启动函数作用为:
- 检索指向新进程的命令行指针
- 检索指向新进程的环境变量指针
- 全局变量初始化
- 内存栈初始化
- 调用应用程序入口点函数main或WinMain
- 入口点函数返回时,调用C运行库的exit函数,进行一些清理处理,最后调用ExitProcess退出
用户编写的入口点函数地址为:0x00401000
函数
函数包含:函数名、入口参数、返回值、函数功能等部分
函数的间接调用:通过动态生成函数地址进行调用,如:call [4*eax+10h]
函数的传参方式:栈传参、寄存器传参、全局变量传参
-
栈传参
子程序通常使用ebp来对存入栈中的参数进行寻址
栈操作的对象只能是双操作数(4个字节)
使用stdcall调用约定调用子函数:
注:ret指令后加操作数,表示在ret指令后,给栈指针esp + 操作数
可改写为:
注1:enter语句的作用相当于push ebp
;mov ebp, esp
;sub esp xxxx
注2:leave语句的作用相当于add esp xxxx
;pop ebp
-
寄存器传参
寄存器传参没有标准,由平台相关编译器开发人员制定。
绝大部分遵循Fastcall调用约定
C++中非静态类成员函数默认使用thiscall的调用约定译器 Fastcall调用约定 Microsoft Visual C++ 左边的2个不大于4字节的参数分别放在eax和ebx,寄存器用完用栈,其余参数仍然按照从右到左的顺序入栈,被调用者返回前清理传送参数的栈,浮点值/远指针/__int64类型总是使用栈来传递 orland Delphi/C++ 左边的3个不大于4字节的参数分别放在eax、edx、ecx,寄存器用完后其余参数按照从左到右的PASCAL方式入栈 atom C 总是使用寄存器传参,默认第一个参数用eax,第二个ebx,第三个ecx,第四个edx,寄存器用完用栈来传参
thiscall调用约定:函数的参数按照从右到左顺序入栈,被调用函数在返回前清理传送参数的栈,仅增加通过ecx寄存器传送一个额外的参数(this指针)
-
名称修饰约定
名称修饰:为了解决不同作用域或不同参数类型但具有相同函数名的函数,而不破坏现有基于C的链接器
C编译函数名修饰约定:- stdcall调用约定在函数名前加一个_前缀,在后面加一个@符号,及其参数字节数
格式为:"_functionname@number" - __cdecl调用约定仅在函数名前加一个_前缀
格式为:"_functionname" - Fastcall调用约定在函数名前面加一个@符号,在后面加一个@符号,及其参数字节数
格式为:"@functionname@number"
C++编译函数名修饰约定:
- stdcall调用约定以?标识函数的开始,后跟函数名,函数名后面,以@@YG标识参数表的开始,后跟参数表。
参数表第一项为函数的返回值类型,其后以此为参数的数据类型,指针标识在其数据类型前,参数表后面以@Z标识整个名字的结束(如果没有参数,则以Z标识结束)
格式为:"?functionname@@YG*****@Z"或"?functionname@@YG*XZ" - __cdecl调用约定与stdcall调用约定相同,只是参数表开始标识由@@YG改为@@YA
- Fastcall调用约定与stdcall调用约定相同,只是参数表开始标识由@@YG改为@@YI
- stdcall调用约定在函数名前加一个_前缀,在后面加一个@符号,及其参数字节数
-
函数返回值
函数返回值传递的方式:return 操作符,参数按引用方式返回、全局变量返回
1. return 操作符返回值
一般情况下,函数返回值放在eax寄存器中返回,如果需返回数据的大小超过 eax寄存器的容量,其高32位就会放在edx寄存器中。
2. 参数按传引用方式返回值
在调用函数时,把变量的地址作为参数传递给子函数,可以在函数中用间接引用运算符修改调用函数传入参数地址上所对应的值
程序举例:
C源代码:
汇编代码:
数据结构
-
局部变量
局部变量分配空间的方式:栈、寄存器
1.1 利用栈存放局部变量:
使用sub esp, 8
为局部变量分配空间,用[ebp-xxxx]
寻址调用这些分配的局部变量,函数结束前,使用add esp, 8
来清除局部变量分配的空间
部分编译器使用push reg
来取代sub esp, 4
来开辟栈空间子函数中如何区分在使用传入的参数,还是局部变量?
使用参数:ebp + xxxx
使用局部变量:ebp - xxxx1.2 利用寄存器存放局部变量
除了栈用2个寄存器,编译器会用剩下的6个通用寄存器尽可能有效的存放局部变量,如果寄存器不够用,编译器会将局部变量放入栈中 -
全局变量
全局变量作用于整个程序,始终存在,放在全局变量的内存区。
全局变量通常位于数据区块.data的一个固定地址,当程序想要访问全局变量时,一般会用一个固定的硬编码地址直接对内存进行寻址如:
mov eax, dword ptr [4084c0h]
;直接调用全局变量,4084c0h是全局变量地址编译器一般会将全局变量放在一个可读可写的区块里,如果放在只读区块里,就是常量
-
数组
数组在汇编状态下访问通过基址 + 变址的寻址方式实现如:
mov eax, [407030h + eax]
;407030h是基址,eax中存放变址(数组下标 * 元素大小) -
虚函数
虚函数的地址不在编译时确定,在即将进行调用前确定
虚函数表:一个数组,存放类中所有虚函数的地址
调用虚函数时,程序首先去除虚函数表指针,得到虚函数表的地址,再根据这个地址从虚函数表中取出想要调用的虚函数的地址
调用虚函数:ecx作为this指针的载体
-
控制语句
1. if语句
2. switch case语句
一般使用和if语句一样的方式实现,但如果各case的取值表示一个算术级数,将使用一个跳转表实现
举例如下:
对应使用跳转表实现的汇编代码:
指令jmp dword ptr [4*eax + 004010B0]
相当于switch(a)转移指令机器码的计算:
3. 条件设置指令
若允许出现条件分支:
若使用条件设置指令,则不会包含条件分支:
使用条件传输指令cmov和fcmov去除转移指令:
-
循环语句
循环语句在汇编中可以进行地址的反向引用(从高地址走向低地址),其他类型的分支语句都是正向引用(从低地址走向高地址),可以通过此法识别循环语句
循环语句一般使用ecx寄存器作为计数器
举例如下:
-
字符串
C字符串(ASCIIZ字符串),以\0作为结束标志
DOS字符串,以$作为结束标志
PASCAL字符串,没有终止符,但在字符串开头定义了1字节,指示当前字符串长度
Delphi字符串,将PASCAL字符串开头的1字节,放大为2字节或4字节