您的位置 首页 作文网

被调函数通过什么语句 007卡盟_fc2系列

楔子

最近一直在复习cpp的基础概念,但对于cpp中的函数还是有点莫名,直到cpp编译器会把类转换成c中的struct结构体,那么函数呢?对于函数,编译器又做了什么处理?所以针对函数,这里进行一下研究。

一、基本

一个可执行文件,它一般由代码区和数据区组成,在加载到内存中进程地址空间中,它就由代码区、数据区、堆区、栈区和堆区栈区之间的动态变化区域组成。函数会发挥什么作用呢?针对一个函数被调用的过程,就是栈帧的创建到销毁的过程。那么一个栈帧的结构是怎么样的呢?首先,关注下面几个问题:

函数栈帧是什么?函数调用过程函数调用中,参数如何传递?传参顺序?形参是如何通过实参进行实例化?函数如何返回?局部变量的存在是怎么样的?

首先,函数调用时可以循环嵌套的,程序经常在执行的时候,出现对另一个函数的调用,然后在外部函数调用完毕后,再返回原地址继续执行,比如main中调用printf或者cout来输出语句。这种先进后出的方式,栈就很合适应对处理,所以针对函数的调用,往往和栈区挂钩。另外,函数的执行轨迹,就是一个二叉树的遍历过程,所以数的遍历往往和递归或者栈进行挂钩。

在栈区中,有着栈帧的这么一块内存,专门存储函数的局部变量、传入参数和返回地址等信息,针对函数的调用,栈区中都会创建一个新的栈帧,其大小取决于函数局部变量和参数数据量。

如上就是一个函数调用的过程中,栈区和栈帧的变化。

1.1 进门看看

了解函数,绕不开栈区和栈帧,要了解底层架构,接触汇编是比较直接的途径了。如下是一个简单的测试用cpp代码:

#include 

void show(){
std::cout << "Hello, world." << std::endl;
}

int main(){
show();
return 0;
}

然后进行汇编,生成汇编文件查看一下信息,一般可以通过gcc -S的做法生成中间的汇编文件,下面就是g++ -S test.cpp -o test.s得到的test.s文件,查看main和show信息,摘取两部分的结果:

_Z4showv:
.LFB1573:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $32, %rsp
.seh_stackalloc 32
.seh_endprologue
leaq .LC0(%rip), %rdx
movq .refptr._ZSt4cout(%rip), %rcx
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movq .refptr._ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(%rip), %rdx
movq %rax, %rcx
call _ZNSolsEPFRSoS_E
nop
addq $32, %rsp
popq %rbp
ret
main:
.LFB1574:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $32, %rsp
.seh_stackalloc 32
.seh_endprologue
call __main
call _Z4showv
movl $0, %eax
addq $32, %rsp
popq %rbp
ret

嗯,还是不太美观,g++编译时被调函数通过什么语句,添加一下参数-fno-asynchronous-unwind-tables,这样得到的汇编码就比较没换了。

_Z4showv:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
leaq .LC0(%rip), %rdx
movq .refptr._ZSt4cout(%rip), %rcx
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movq .refptr._ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(%rip), %rdx
movq %rax, %rcx
call _ZNSolsEPFRSoS_E
nop
leave
ret
main:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
call __main
call _Z4showv
movl $0, %eax
leave
ret

从上面就可以看得出,两个函数的汇编码都有着比较一致的结构,作为开始的pushq、movq和subq三剑客开头,leave和ret结尾,并且中间才是正经处理。首先关注开头部分rbp和rsp两个寄存器。要知道的是,在很多的解释中,这两个寄存器应该是EBP和ESP才对,这里的rbp和rsp是对应的当前x86_64架构,前面的是针对简单x86,所以才有不同。那么它们发挥什么作用呢?

rbp和rsp,前者是函数调用前保存函数栈帧基址的寄存器,函数被调用后,创建一个新的栈帧,rbp寄存器就存储这个栈帧的基址,通过rbp寄存器就可以很方便的访问栈帧中数据;后者是指示栈区顶部地址的寄存器,要知道的是,栈区是位于进程地址空间中最高地址的部分,主要从上而下进行扩展,所以它的顶部,也就是栈区中实际地址最小的地方,当函数调用开始,它会继续向下移动,为新的栈帧分配内存;另外的rip,这个寄存器保存的是指令地址,在函数调用后,会指向被调用函数的入口地址,开始函数指令的调用,函数调用结束以后,rip就恢复到调用者调用函数指令的下一条指令。

简单来说,看指令调用,可以查rip,看栈帧地址,就看rsp,看栈基址就看rbp。另外,生成汇编码的办法,除了上面gcc -S以外,还可以使用objdump来针对可执行文件进行汇编文件的生成,这也是反汇编的一部分:

objdump.exe -s -d test.exe > test.s

因为是windows下进行实验,所以各种文件后缀都是exe,而且这是配备了mingw环境的。

1.2 进入实际调用

查看汇编文件,主要是确定思路,但实际中运行得怎么样,还是得看gdb,下面就使用gdb进行调试看看运行如何:

函数调用语句由什么组成_函数语句调用_被调函数通过什么语句

(gdb) disass main
Dump of assembler code for function main():
0x000000000040157d : push %rbp
0x000000000040157e : mov %rsp,%rbp
0x0000000000401581 : sub $0x20,%rsp
0x0000000000401585 : callq 0x4016e0
0x000000000040158a : callq 0x401550
0x000000000040158f : mov $0x0,%eax
0x0000000000401594 : leaveq
0x0000000000401595 : retq
End of assembler dump.
(gdb) disass show

Dump of assembler code for function show():
0x0000000000401550 : push %rbp
0x0000000000401551 : mov %rsp,%rbp
0x0000000000401554 : sub $0x20,%rsp
0x0000000000401558 : lea 0x2aa2(%rip),%rdx # 0x404001
0x000000000040155f : mov 0x2daa(%rip),%rcx # 0x404310
0x0000000000401566 : callq 0x401600
0x000000000040156b : mov 0x2dae(%rip),%rdx # 0x404320
0x0000000000401572 : mov %rax,%rcx
0x0000000000401575 : callq 0x401620
0x000000000040157a : nop
0x000000000040157b : leaveq
0x000000000040157c : retq
End of assembler dump.
(gdb)

如上,在实际运行的时候,没有函数名的存在,直接就是机器地址,这个地址对应进程空间中的具体位置,只能是只读代码区中。另外,要查看rip等寄存器信息,可以进入gdb以后使用info r rip或者x /x $rip进行,下面针对show函数调用前后来针对性查看寄存器信息:

(gdb) b 8
Breakpoint 1 at 0x40158a: file test.cpp, line 8.
(gdb) r
Starting program: D:Desktoptesttest.exe
[New Thread 9208.0x1860]
[New Thread 9208.0x2f0c]
[New Thread 9208.0x299c]
[New Thread 9208.0x1bdc]

Thread 1 hit Breakpoint 1, main () at test.cpp:8
8 show();
(gdb) bt
#0 main () at test.cpp:8
(gdb) info r rip
rip 0x40158a 0x40158a

(gdb) info r rsp
rsp 0x61fe00 0x61fe00
(gdb) info r rbp
rbp 0x61fe20 0x61fe20
(gdb)

如上,打断点在show函数执行被调函数通过什么语句,然后单步执行到断点处,然后查看堆栈信息。info r得到的结果是对照寄存器名、寄存器的值,查看rip寄存器信息可以发现,当前指令执行,到了相对起始偏移13的位置,对照前面main汇编码刚好是callq 0x401550 指令。然后使用x命令查看一下:

(gdb) x /x $rip
0x40158a
: 0xffffc1e8
(gdb) x /x $rbp
0x61fe20: 0x00621960
(gdb) x /x $rsp
0x61fe00: 0x00000008

和上面的info得到的信息没多大区别,然后进行函数调用,step进入函数:

(gdb) step
show () at test.cpp:3
3 void show(){
(gdb) bt
#0 show () at test.cpp:3
#1 0x000000000040158f in main () at test.cpp:8
(gdb) info r rip
rip 0x401550 0x401550
(gdb) x /x $rbp
0x61fe20: 0x0000000000621960
(gdb) x /x $rsp
0x61fdf8: 0x000000000040158f

被调函数通过什么语句_函数调用语句由什么组成_函数语句调用

如上,rip的值成了0x401550,查看main汇编,刚好是指令中显示的show的函数地址,rsp的值是0x000000000040158f,对应的是main中函数调用以后的指令地址,现在rbp的值,应该指向的是新的栈帧的地址。继续next执行函数,看看它们又有什么变化:

(gdb) next
4 std::cout << "Hello, world." << std::endl;
(gdb) x /x $rip
0x401558 : 0x4800002aa2158d48
(gdb) x /x $rbp
0x61fdf0: 0x000000000061fe20
(gdb) x /x $rsp
0x61fdd0: 0x0000000000401630

现在进入show函数执行以后,rip的值变成了0x401558,这是show中在开始三件套以后的正常执行的第一条指令,而rbp和rsp的值有所改变,两个的差值和在main中的时候,差值一样,把一个栈帧的范围给确定了下来。然后执行完show,退出来又恢复原样了:

(gdb) next
[New Thread 9208.0x1260]
Hello, world.
5 }
(gdb) next
main () at test.cpp:9
9 return 0;
(gdb) info r rip
rip 0x40158f 0x40158f

(gdb) info r rbp
rbp 0x61fe20 0x61fe20
(gdb) info r rsp
rsp 0x61fe00 0x61fe00
(gdb)

rip依旧执行main中特定指令地址,rsp和rbp依然指定main中栈帧的范围。以上就是一个函数中栈帧的重要内容,rip确定指令执行,rbp和rsp进行栈帧范围确定,可惜的是,这个流程依然还是比较模糊,不够清楚明了。

1.3 传参

修改一下上面示例代码:

被调函数通过什么语句_函数调用语句由什么组成_函数语句调用

#include 
#include

void show(const std::string& name){
std::cout << "Hello, " << name << std::endl;
}

int main(){
show("Jack");
return 0;
}

如上,传入一个string的字符串常量,进行输出,然后gdb调试:

(gdb) b 9
Breakpoint 1 at 0x4015a4: file test.cpp, line 9.
(gdb) r
Starting program: D:Desktoptesttest.exe
[New Thread 5704.0x19fc]
[New Thread 5704.0x3f6c]
[New Thread 5704.0x1020]
[New Thread 5704.0x4d1c]

Thread 1 hit Breakpoint 1, main () at test.cpp:9
9 show("Jack");
(gdb) frame
#0 main () at test.cpp:9
9 show("Jack");
(gdb) step
show (name="Jack") at test.cpp:5
5 std::cout << "Hello, " << name << std::endl;
(gdb) frame
#0 show (name="Jack") at test.cpp:5
5 std::cout << "Hello, " << name << std::endl;
(gdb) info args
name = "Jack"
(gdb) info locals
No locals.
(gdb)

如上,step进入函数,info args查看函数参数,info args查看局部变量,不过引入传入的是引用,而引用的是字符串常量,所以没有局部变量。然后修改一下上面示例代码,添加上局部变量name的声明定义,再传入show中进行调用,就有局部变量了:

(gdb) frame
#0 main () at test.cpp:10
10 show(name);
(gdb) info locals
name = "Jack"
(gdb) step
show (name="Jack") at test.cpp:5
5 std::cout << "Hello, " << name << std::endl;
(gdb) info args
name = "Jack"
(gdb) info locals
No locals.
(gdb)

如上,在main中,locals可以查看局部变量是name,进入show函数栈帧后,查看到参数是name,但没有局部变量,因为是引用嘛,熟悉函数传参的都知道,传入引用或者指针,函数都是直接在本体上进行操作的,如果传入的是简单拷贝,那就会有局部变量的出现,那再改一改,把show的参数改为简单传参,而不是引用或者指针。

(gdb) r
Starting program: D:Desktoptesttest.exe
[New Thread 2368.0x1b98]
[New Thread 2368.0x39b0]
[New Thread 2368.0x2a1c]
[New Thread 2368.0x2b60]

Thread 1 hit Breakpoint 1, main () at test.cpp:10
10 show(name);
(gdb) info locals
name = "Jack"
(gdb) step
show (name="Jack") at test.cpp:5
5 std::cout << "Hello, " << name << std::endl;
(gdb) info args
name = "Jack"
(gdb) info locals
No locals.
(gdb)

出乎意料的,上面的传参中,就算name是一份main中的string拷贝,在show中依然没有局部变量,只有传入参数,长见识了。看来只有在函数中进行声明定义的,才会有局部变量的存在了。嗯,在show中简单声明了一个int型变量i,然后info locals就可以查看到局部变量存在这么一个i了。函数的结构就这样,下一节研究一下cpp和c中的函数的差异,以及成员函数到底是个怎么回事,请待后续。

本文来自网络,不代表立场,转载请注明出处:https://www.looktm.com.cn/zuowen/76142.html

发表回复

联系我们

联系我们

0898-88881688

在线咨询: QQ交谈

邮箱: email@wangzhan.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部