第一节、函数及函数调用过程
一、函数简介
函数的组成:
(1)、函数名:其定义规则和变量名的定义规则一样,是一个符合C++语法要求的标识符,但尽量不要用下划线开头,以避免和编译器的变量或函数重名。
(2)、参数:可以有0或多个,用于向函数传送数值或从函数带回数值。每个参数都有自己的类型。
(3)、返回值:指定函数返回的值。若没有返回值,则返回类型位void。
(4)、函数体:花括号中完成函数功能的语句。
举例,编写一个函数,用于获取两个整数中的较大值:
int max(int a, int b) {
if (a > b)
return a;
else
return b;
}
二、函数调用过程
函数的一般调用过程如下:
(1)、将函数参数入栈,第一个参数在栈顶,最后一个参数在栈底(由右向左压栈)。
(2)、执行CALL指令,调用该函数,进入该函数代码空间。
(3)、将CALL指令下一行代码的地址入栈。
(4)、进入函数代码空间后,将基址指针EBP入栈,然后让基址指针EBP指向当前栈的栈顶,并用它访问存在栈中的函数输入参数及栈中的其他数据。
(5)、栈指针ESP减少一个值,即向低位移动一段距离,用来预留一段空间给该函数作为临时存储区。
(6)、函数正式被执行。
(7)、函数执行完毕返回调用处继续执行后继指令。
执行完被调用的函数之后,将当前EBP的值传给堆栈指针ESP,恢复ESP,基址指针EBP出栈,恢复EBP,最后执行RET指令,返回调用点的下一条指令。
例如:调用一个带有两个参数的函数:
push 参数2
push 参数1
call 函数地址
push ebp
mov ebp esp
sub esp,xxx
函数返回时,则有
add esp,xxx
pop ebp
retn
在函数的调用过程中,会有相应的函数调用约定。常用的调用约定有:C调用约定、标准调用约定、FastCall调用约定和C++调用约定。
1、C调用约定
C语言调用约定又称cdecl调用约定,是x86体系结构中C编译器使用的默认调用约定。该调用约定规定调用方将参数按照从右往左的顺序入栈。在调用完成之后同样由调用方将参数从堆栈中清除。
举例如下,在主程序输入两个数值,调用上述函数输出较大值:
#include<iostream>
using namespace std;
int max(int a, int b);
int main() {
int x, y,above;
cout << "请输入两个整数(用空格隔开):";
cin >> x >> y;
above = max(x, y);
cout << "您输入的较大的值为:" << above << endl;
system("pause");
return 0;
}
int max(int a, int b) {
if (a > b)
return a;
else
return b;
}
用IDA打开可执行文件,找到函数的调用过程如下:参数从右到左入栈,然后调用函数sub_41132A
2、标准调用约定
标准调用约定:是指在函数声明过程中使用了_stdcall修饰符。如:
int _stdcall max(int a,int b);
它和C调用约定唯一的区别就是,标准调用约定在调用完成之后调用方无需从堆栈中删除参数,由被调用方在调用完成时删除堆栈中的函数参数。
再次运行生成可执行文件,然后用IDA打开,如下,函数调用之后,比上面的少了一条指令:add esp,8
3、FastCall调用约定
FastCall调用约定可以看作是stdcall约定的一种改进版,它向寄存器(ECX和EDX)最多传递两个参数,若参数大于两个,则按照stdcall约定,参数入栈。函数返回时由被调用方从堆栈中删除参数。
求三个数的最大值,程序如下:
#include<iostream>
using namespace std;
int max(int a, int b);
int _fastcall Max(int a, int b, int c);
int main() {
int x, y,z,above;
cout << "请输入三个整数(用空格隔开):";
cin >> x >> y >> z;
above = Max(x, y, z);
cout << "您输入的较大的值为:" << above << endl;
system("pause");
return 0;
}
int max(int a, int b) {
if (a > b)
return a;
else
return b;
}
int _fastcall Max(int a, int b, int c) {
int t;
t = max(a, b);
if (c > t)
return c;
else
return t;
}
用IDA打开如下:
将最后一个参数入栈,第二个参数送入edx,然后把第一个参数送入ecx.
第二节、启动函数和入口点
一、启动函数
操作系统实际上并不调用我们所写的入口点函数,而是调用由C/C++运行库实现并在链接时使用-entry:命令行选项来设置的一个C/C++运行时启动函数。所有启动函数所做的工作大致相同。启动函数执行的功能如下:
(1)获取指向新进程的完整命令行的一个指针;
(2)获取指向新进程的环境变量的一个指针;
(3)初始化C/C++运行库的全局变量;
(4)初始化C运行库内存分配函数以及其他的一些底层的I/O例程使用的堆;
(5)调用所有全部和静态C++类对象的构造函数。
二、入口点
完成启动函数的所有操作之后,C/C++就会调用相应的入口点函数。在PE文件中入口点是一个内存地址。
入口点函数的原型如下:
int WINAPI WinMain(
HINSTANCE hInstanceExe, //实际值为一个内存基地址
HINSTANCE, //用于16位系统,对32位系统都位NULL
PTSTR pszCmdLine, //用来运行程序的命令行
int nCmdShow); //用来指明程序最初如何显示
第三节、控制语句
一、if-else语句
编写代码,判断输入的年份是否正确,如下:test.cpp
#include <windows.h>
#include<iostream>
using namespace std;
void main()
{
int year;
cout << "请输入年份:";
cin >> year;
if (year == 2018) {
MessageBox(NULL, "恭喜你,输入正确...", "验证通过", MB_OKCANCEL);
}
else {
MessageBox(NULL, "输入错误...", "验证失败", MB_OKCANCEL);
}
}
运行程序:
用IDA打开程序,程序结构如下:在IDA中,红色箭头表示False,绿色箭头表示TRUE。通过如下视图可清晰看到程序的两个分支结构,且不存在循环:
通过观察汇编代码,可发现关键跳转:jnz short loc_4123FC.在破解时只需修改该跳转即可绕过验证。
也可以利用IDA的F5插件功能,得到程序的伪代码,从程序中便可直接看到明文给出的验证数字2018如下:
在反汇编时,会发现for循环和while循环的操作过程和if-else语句类似,只是该语句的循环进行。