从汇编语言进一步了解C++三种函数参数传递方式

时间:2022-05-24 01:25:19

前言:

本文为另一篇博文C++函数参数传递的一大利器——引用(左值)中对函数参数传递方式的进一步探讨,部分内容涉及汇编语言,但不影响理解。


—————————————————————————————————————————————————————————————————————————————

编译器对函数调用的实现过程:

首先我们要知道,计算机对数据的处理是不断寻址、读写数据的过程。上升到程序层面,一个个函数在汇编语言中就是一段段汇编指令,这些汇编指令让计算机通过寄存器在内存中访问指定内存块地址并从中读写数据。除全局变量和其它一些形式定义的变量外,函数中的变量都存在栈内存中,程序对变量的修改、传递等一系列操作都是寄存器通过将内容或地址进栈、出栈来实现的。

下面看代码:

C++源代码:

#include <iostream>

using namespace std;

void swap1(int *const a, int *const b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}

void swap2(int &a, int &b)
{
int temp;
temp = a;
a = b;
b = temp;
}

void swap3(int a, int b)
{
int temp;
temp = a;
a = b;
b = temp;
}

void swap4(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}

int main()
{
int x = 3;
int y = 5;
int *p1 = &x, *p2 = &y;
cout << "初始值:" << "x = " << x << ",y = " << y << endl;
swap1(p1,p2);
cout << "执行swap1后:" << "x = " << x << ",y = " << y << endl;
swap2(x,y);
cout << "执行swap2后:" << "x = " << x << ",y = " << y << endl;
swap3(x, y);
cout << "执行swap3后:" << "x = " << x << ",y = " << y << endl;
swap4(p1, p2);
cout << "执行swap4后:" << "x = " << x << ",y = " << y << endl;
}

从汇编语言进一步了解C++三种函数参数传递方式

编译、执行上述程序后,结果显而易见:除了swap3函数以外,其余函数都能成功交换xy的值。


接下来找到这段程序编译后生成的汇编代码(某些编译器需要设置为将程序翻译成汇编指令),我是把编译后生成的机器码反汇编为汇编代码。

下面是C++程序的汇编形式(swap函数和main函数部分):

汇编代码:

__Z5swap1PiS_:
1000010a0:55 pushrbp
1000010a1:48 89 e5 movrbp, rsp
1000010a4:48 89 7d f8 movqword ptr [rbp - 8], rdi
1000010a8:48 89 75 f0 movqword ptr [rbp - 16], rsi
1000010ac:48 8b 75 f8 movrsi, qword ptr [rbp - 8]
1000010b0:8b 06 moveax, dword ptr [rsi]
1000010b2:89 45 ec movdword ptr [rbp - 20], eax
1000010b5:48 8b 75 f0 movrsi, qword ptr [rbp - 16]
1000010b9:8b 06 moveax, dword ptr [rsi]
1000010bb:48 8b 75 f8 movrsi, qword ptr [rbp - 8]
1000010bf:89 06 movdword ptr [rsi], eax
1000010c1:8b 45 ec moveax, dword ptr [rbp - 20]
1000010c4:48 8b 75 f0 movrsi, qword ptr [rbp - 16]
1000010c8:89 06 movdword ptr [rsi], eax
1000010ca:5d poprbp
1000010cb:c3 ret
1000010cc:0f 1f 40 00 nopdword ptr [rax]

__Z5swap2RiS_:
1000010d0:55 pushrbp
1000010d1:48 89 e5 movrbp, rsp
1000010d4:48 89 7d f8 movqword ptr [rbp - 8], rdi
1000010d8:48 89 75 f0 movqword ptr [rbp - 16], rsi
1000010dc:48 8b 75 f8 movrsi, qword ptr [rbp - 8]
1000010e0:8b 06 moveax, dword ptr [rsi]
1000010e2:89 45 ec movdword ptr [rbp - 20], eax
1000010e5:48 8b 75 f0 movrsi, qword ptr [rbp - 16]
1000010e9:8b 06 moveax, dword ptr [rsi]
1000010eb:48 8b 75 f8 movrsi, qword ptr [rbp - 8]
1000010ef:89 06 movdword ptr [rsi], eax
1000010f1:8b 45 ec moveax, dword ptr [rbp - 20]
1000010f4:48 8b 75 f0 movrsi, qword ptr [rbp - 16]
1000010f8:89 06 movdword ptr [rsi], eax
1000010fa:5d poprbp
1000010fb:c3 ret
1000010fc:0f 1f 40 00 nopdword ptr [rax]

__Z5swap3ii:
100001100:55 pushrbp
100001101:48 89 e5 movrbp, rsp
100001104:89 7d fc movdword ptr [rbp - 4], edi
100001107:89 75 f8 movdword ptr [rbp - 8], esi
10000110a:8b 75 fc movesi, dword ptr [rbp - 4]
10000110d:89 75 f4 movdword ptr [rbp - 12], esi
100001110:8b 75 f8 movesi, dword ptr [rbp - 8]
100001113:89 75 fc movdword ptr [rbp - 4], esi
100001116:8b 75 f4 movesi, dword ptr [rbp - 12]
100001119:89 75 f8 movdword ptr [rbp - 8], esi
10000111c:5d poprbp
10000111d:c3 ret
10000111e:66 90 nop

__Z5swap4PiS_:
100001120:55 pushrbp
100001121:48 89 e5 movrbp, rsp
100001124:48 89 7d f8 movqword ptr [rbp - 8], rdi
100001128:48 89 75 f0 movqword ptr [rbp - 16], rsi
10000112c:48 8b 75 f8 movrsi, qword ptr [rbp - 8]
100001130:8b 06 moveax, dword ptr [rsi]
100001132:89 45 ec movdword ptr [rbp - 20], eax
100001135:48 8b 75 f0 movrsi, qword ptr [rbp - 16]
100001139:8b 06 moveax, dword ptr [rsi]
10000113b:48 8b 75 f8 movrsi, qword ptr [rbp - 8]
10000113f:89 06 movdword ptr [rsi], eax
100001141:8b 45 ec moveax, dword ptr [rbp - 20]
100001144:48 8b 75 f0 movrsi, qword ptr [rbp - 16]
100001148:89 06 movdword ptr [rsi], eax
10000114a:5d poprbp
10000114b:c3 ret
10000114c:0f 1f 40 00 nopdword ptr [rax]

_main:
100001150:55 pushrbp
100001151:48 89 e5 movrbp, rsp
100001154:48 83 ec 30 subrsp, 48
100001158:48 8d 45 e8 learax, [rbp - 24]
10000115c:48 8d 4d ec learcx, [rbp - 20]
100001160:c7 45 ec 03 00 00 00 movdword ptr [rbp - 20], 3
100001167:c7 45 e8 05 00 00 00 movdword ptr [rbp - 24], 5
10000116e:48 89 4d e0 movqword ptr [rbp - 32], rcx
100001172:48 89 45 d8 movqword ptr [rbp - 40], rax
100001176:48 8b 7d e0 movrdi, qword ptr [rbp - 32]
10000117a:48 8b 75 d8 movrsi, qword ptr [rbp - 40]
10000117e:e8 1d ff ff ff call-227 <__Z5swap1PiS_>
100001183:48 8d 7d ec leardi, [rbp - 20]
100001187:48 8d 75 e8 learsi, [rbp - 24]
10000118b:e8 40 ff ff ff call-192 <__Z5swap2RiS_>
100001190:8b 7d ec movedi, dword ptr [rbp - 20]
100001193:8b 75 e8 movesi, dword ptr [rbp - 24]
100001196:e8 65 ff ff ff call-155 <__Z5swap3ii>
10000119b:48 8b 7d e0 movrdi, qword ptr [rbp - 32]
10000119f:48 8b 75 d8 movrsi, qword ptr [rbp - 40]
1000011a3:e8 78 ff ff ff call-136 <__Z5swap4PiS_>
1000011a8:48 8b 3d 61 0e 00 00 movrdi, qword ptr [rip + 3681]
1000011af:8b 75 ec movesi, dword ptr [rbp - 20]
1000011b2:e8 85 0b 00 00 call2949
1000011b7:48 8d 35 5e 0d 00 00 learsi, [rip + 3422]
1000011be:48 89 c7 movrdi, rax
1000011c1:e8 3a 00 00 00 call58

在我们编译完成C++程序后,从生成的汇编指令可以看到计算机已经为每个函数(包括main函数)需要用到的变量分配好了内存空间。

函数调用过程:

博主对汇编的了解也是半吊子水平,上面的汇编指令虽然看得懂但无法组织语言详细描述出来,这些汇编指令做的工作在本文开头已经简要介绍过。下面只针对main函数各个swap函数做简要说明:


int main (void)

程序从main函数开始,首先设置好诸如数据地址和栈地址等等以便计算机能通过寄存器找到,然后为xy变量分配好了内存,往内存中写入数据并将其内存地址进栈,然后为指针变量分配内存,往里面写入xy的地址,将指针变量的地址进栈。接着当main函数调用swap1函数时,程序通过段地址:偏移地址跳到swap1函数执行,(中间过程在swap函数中)执行完成后回到main函数接着执行,以此类推。


void swap1 (int* const a, int* const b)

swap1的形参是两个const指针。swap1将栈内存中的p1p2的地址中的内容,也就是xy的地址取出作为swap1int* const aint* const b的内容(实际上并没有为ab这两个指针变量分配内存),为temp变量分配内存。因此swap1函数能根据xy的地址对xy的值进行操作。


void swap2(int &a,int &b)

swap2的形参是两个引用变量。swap2函数将栈内存中xy的地址取出作为swap2 int& aint& b的内容(和swap1一样也没有为形参分配内存)。因此形参为引用变量的函数本质上也是通过地址对xy的值进行操作。


void swap3(int a,int b)

swap3的形参是两个普通int型变量。函数将xy的值(也就是变量xy分配到的地址块中存放的内容)作为ab的内容(也就是之前博文说的xy副本),所以swap3函数并不能对xy的值产生影响。


void swap4(int *a,int *b)

swap4的形参是两个普通int型指针变量。与swap1函数一样,swap4将实参p1p2存放的内容,即xy的地址作为形参,通过地址对xy的值进行操作。


总结:

单从swap1~swap44个子函数本身来说,它们并没有为形参分配内存(所以叫形参),简单来说就是函数会根据形参表来确定它要使用的是内容还是地址(实际上都是栈内存的地址中存放的内容区别在于栈内存中的内容有些是另一个变量的内容有些则是另一个变量的地址)。而结合main函数,由于形参是指针的swap1swap4,而main函数为p1p2两个int型指针变量分配了内存,因此比形参引用的swap2多了一个中间步骤。从效率上来说,swap2swap3的效率要略高于swap1swap4,又由于按引用传递参数既可以影响实参值也可以配合const产生临时变量而不影响实参(讲引用的博文有介绍),加之指针强大但难以控制的特性,使引用往往成为许多情况下的最佳选择。



本文程序所用IDE为Xcode 8.2