C++ 中函数的调用包含参数入栈、函数跳转、保护现场、回复现场等过程,重点过程如下:
(1)将函数的参数压入栈中,从右至左压入。
(2)调用函数时,将当前程序的执行位置(即返回地址)压入栈中。
(3)将函数的栈帧(也称为活动记录)压入栈中。栈帧包含了函数的局部变量、函数返回值、函数的上一级调用者的栈帧指针等信息。
(4)执行函数体内的语句,包括局部变量的声明和初始化、函数体语句的执行等。
(5)函数执行完毕后,将函数的返回值保存在寄存器中(或者栈中)。
(6)弹出函数的栈帧,并将返回值传递给上一级函数。
(7)将返回地址弹出栈中,程序跳转到该地址继续执行。
以如下代码为例( 64 位程序):
#include
int add(int a, int b)
{
int sum = a + b;
return sum;
}
int main()
{
int sum = add(1, 2);
return 0;
}
首先给 main()
函数的第一行 int sum = add(1, 2);
打上断点,调试运行程序。
程序暂停后,查看当前汇编代码( VS2017 查看方法:右击当前代码页,选择转到反汇编
):
int main()
{
00007FF67D8AA630 push rbp
00007FF67D8AA632 push rdi
00007FF67D8AA633 sub rsp,108h
00007FF67D8AA63A lea rbp,[rsp+20h]
00007FF67D8AA63F mov rdi,rsp
00007FF67D8AA642 mov ecx,42h
00007FF67D8AA647 mov eax,0CCCCCCCCh
00007FF67D8AA64C rep stos dword ptr [rdi]
00007FF67D8AA64E lea rcx,[__81FC6F77_main2@cpp (07FF67D9E41D7h)]
00007FF67D8AA655 call __CheckForDebuggerJustMyCode (07FF67D874108h)
int sum = add(1, 2);
00007FF67D8AA65A mov edx,2
00007FF67D8AA65F mov ecx,1
00007FF67D8AA664 call add (07FF67D87584Bh)
00007FF67D8AA669 mov dword ptr [sum],eax
return 0;
00007FF67D8AA66C xor eax,eax
}
在汇编代码中,程序暂停在第 14 行(00007FF67D8AA65A mov edx,2
)。后面的两行是传入参数的过程,其中,edx是数据寄存器,常用于存储一些大于 AX 寄存器的 16 位数和 32 位数的运算中的高位数。在函数调用中, edx 寄存器用于存储第一个参数值。ecx是计数寄存器,常用于存储循环计数器和移位操作的计数器。在函数调用中, ecx 寄存器用于存储第二个参数值。通过这两行传入的值可以看出,调用函数时,参数入栈时从右往左。
汇编行00007FF67D8AA664 call add (07FF67D87584Bh)
用于跳转到待调用的函数内,但这里需要注意的是,地址07FF67D87584Bh
并不是待调用的函数的地址,该代码会执行到下面这一行:
00007FF67D87584B jmp add (07FF67D8AA5C0h)
这里的地址07FF67D8AA5C0h
才是真正待调用函数的地址。下面即进入被调用函数内部:
int add(int a, int b)
{
00007FF67D8AA5C0 mov dword ptr [rsp+10h],edx
00007FF67D8AA5C4 mov dword ptr [rsp+8],ecx
00007FF67D8AA5C8 push rbp
00007FF67D8AA5C9 push rdi
00007FF67D8AA5CA sub rsp,108h
00007FF67D8AA5D1 lea rbp,[rsp+20h]
00007FF67D8AA5D6 mov rdi,rsp
00007FF67D8AA5D9 mov ecx,42h
00007FF67D8AA5DE mov eax,0CCCCCCCCh
00007FF67D8AA5E3 rep stos dword ptr [rdi]
00007FF67D8AA5E5 mov ecx,dword ptr [rsp+128h]
00007FF67D8AA5EC lea rcx,[__81FC6F77_main2@cpp (07FF67D9E41D7h)]
00007FF67D8AA5F3 call __CheckForDebuggerJustMyCode (07FF67D874108h)
int sum = a + b;
00007FF67D8AA5F8 mov eax,dword ptr [b]
00007FF67D8AA5FE mov ecx,dword ptr [a]
00007FF67D8AA604 add ecx,eax
00007FF67D8AA606 mov eax,ecx
00007FF67D8AA608 mov dword ptr [sum],eax
return sum;
00007FF67D8AA60B mov eax,dword ptr [sum]
}
这段汇编代码的第 2 行到第 15 行之间是对该函数的栈初始化工作,由编译器自动添加。其中 rsp ( 32 位程序中是 esp ) 、rbp ( 32 位程序中是 ebp )、rdi ( 32 位程序中是 edi )是常用的寄存器:
rsp 为栈指针,常用来指向栈顶。上面汇编代码中第 6 行00007FF67D8AA5CA sub rsp,108h
的意思是将栈顶指针往上移动 108h Byte。这个区域为间隔空间,将被调用的 add 函数与 main 函数的栈区域隔开一段距离,同时还要预留出存储局部变量的内存区域。
rbp 为基址指针,常用来指向栈底。
rdi 为目的变址寄存器。
上面汇编代码的第 17 行到第 21 行之间是进行两数相加的逻辑操作。
执行到第最后一行后打开寄存器查看器( VS2017 查看方法:调试–>窗口–>寄存器),可以查看到如下值:
RAX = 0000000000000003 RBX = 0000000000000000 RCX = 0000000000000003 RDX = 0000000000000002 RSI = 0000000000000000 RDI = 0000005BD30FFA58 R8 = 0000020993014F70 R9 = 0000005BD30FF954 R10 = 0000000000000013 R11 = 00000209930242E0 R12 = 0000000000000000 R13 = 0000000000000000 R14 = 0000000000000000 R15 = 0000000000000000 RIP = 00007FF67D8AA60B RSP = 0000005BD30FF950 RBP = 0000005BD30FF970 EFL = 00000206
0x0000005BD30FF974 = 00000003
查看寄存器 RDI 的内存值( VS2017 查看方法:调试–>窗口–>内存->内存1):
0000005bd30ffb78 0000005bd30ffa90 00007ff67d8aa669 00007ff600000001 cccccccc00000002 cccccccccccccccc cccccccccccccccc cccccccccccccccc cccccccccccccccc cccccccccccccccc cccccccccccccccc
其中第三个值 00007ff67d8aa669
是 main 函数中调用该函数后的下一行汇编代码。
至此,整个调用过程结束。
由于浮点数存入时有可能因为四舍五入而造成精度损失,所以两个浮点数直接用==
操作符进行比较很可能会得到不符合预期的结果。
浮点数的比较应该使用如下方式:
对于浮点数而言比较合适的精度为:0.000001
对于双精度浮点数而言比较合适的精度为:0.0000000000000001
因此可以定义两个宏:
#define ACCURACY_F 1e-6
#define ACCURACY_D 1e-16
判断浮点数是否等于 0 :
float 类型:if(fabs(f) <= ACCURACY_F );
double 类型:if(fabs(d) <= ACCURACY_D);
判断两个浮点数是否相等:
float 类型:if(fabs(f1 - f2) <= ACCURACY_F);
double 类型:if(fabs(d1 - d2) <= ACCURACY_D);