linux64 g 编译器,使用Linux工具分析g++生成的代码

66b52468c121889b900d4956032f1009.png

8种机械键盘轴体对比

本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?

近来想用实际代码实验来验证《Effective C++》、《深度探索C++对象模型》中的知识,通过反汇编等手段查看编译器生成的代码,原本想着看能不能设置好编译参数,使得编译器可以输出书本中的中间代码,可惜的是暂时还没找到,还一度以为只能通过强行分析汇编码。经过一周的摸索,总算弄出了一个可以接受的方案,这篇博客主要说明几个命令之间生成代码的信息。

环境支持Linux

g++

gdb

nm

objdump

c++filt

readelf

其中g++本身就可以通过添加-S参数就可以生成汇编码了,nm命令用来查看生成二进制文件的函数表,objdump是查看更多关于二进制文件的信息,c++filt命令使用来demangling,readelf用于读取ELF文件信息。

目标

《深度探索C++对象模型》中的构造语义章节中提到过,如果有一个Object类,而在声明一个实例的时候:1Object obj;

编译器可能会生成如下的中间代码:1

2Object obj;

Object::Object(&obj);

而生成的函数原型是:1void Object::Object(Object* const this);

此实验的目的就是验证编译器确实是生成了这样的中间代码。结论是目前只有gdb能做到,在呈现成果的结果前,也试试用别的命令看看能够尝试到什么样的结果。

x86_64汇编相关的准备知识

一旦开始做这类C++相关代码分析的工作,汇编是逃不开的,不过现在也基本不用写,会读就可以了。g++生成的汇编码是AT&T格式的,64位机的条件下,每个涉及到操作数的命令中后面会带有b、w、l、q等字母,分别代表操作一个1Byte(8 bit)、2 Byte(16bit)、4 Byte(32bit)以及8 Byte(64bit)。例如:movb %al, %bl代表把ax寄存器的低8bit赋值给bx寄存器的低8bit。

movw %ax, %bx代表把ax中16bit的值赋值给bx寄存器。

movl %eax, %ebx代表把eax中32bit的值赋值予ebx寄存器。

movq %rax, %rbx代表把rax中64bit的值赋予rbx寄存器。

x86_64有16个64bit寄存器,分别为%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。笔者所用的Linux下的g++编译器一般会这样划分寄存器的用途:%rax 作为函数返回值使用。

%rsp 栈指针寄存器,指向栈顶。

%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数…

%rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改。

%r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值。

实验用的代码1

2

3

4

5

6

7

8

9

10

11

12

13class

{

public:

Object()

{}

private:

};

int main(int argc, char* argv[])

{

Object obj;

return 0;

}

这里需要定义Object::Object(),因为编译器在这里不会自动合成Object类的构造函数。如果连Object类是个空类,没有函数成员,也没有数据成员,编译器会为这个类生成怎样的代码?这个问题也在《深度探索C++对象模型》中提到过,有兴趣者可以通过搜索空基类优化关键字获得相关的知识,这里就不展开细说了。

nm命令

nm命令是用于列出目标文件中的符号,在这个实验中用作输出函数签名,查找看能不能输出期望的void Object::Object(Object * const)的函数签名。

首先使用1$ g++ main.cpp

生成目标文件a.out。随后:1$ nm a.out | c++filt

可以看到终端输出(经过处理):1

2

30000000000400566 T main

0000000000400588 W Object::Object()

0000000000400588 W Object::Object()

信息还不少,可惜没有找到期待的void Object::Object(Object * const)。

objdump

不改动上述的a.out文件,执行:1$ objdump -t a.out | c++filt

也是输出了不少信息(经过处理):1

2

30000000000400566 g F .text0000000000000022 main

0000000000400588 w F .text000000000000000b Object::Object()

0000000000400588 w F .text000000000000000b Object::Object()

当前只关注能不能找到void Object::Object(Object * const),所以这里也不展开介绍这个命令输出的内容。

readelf

不改动上述的a.out文件,执行:1$ readelf -s a.out | c++filt

也是输出了不少信息(经过处理):1

2

355: 0000000000400588 11 FUNC WEAK DEFAULT 11 Object::Object()

60: 0000000000400588 11 FUNC WEAK DEFAULT 11 Object::Object()

63: 0000000000400566 34 FUNC GLOBAL DEFAULT 11 main

也能获得不少关于符号表中的信息,可惜没有期待的void Object::Object(Object * const)。

直接查看g++生成的汇编码

还是上述的C++代码,不过不是分析编译器生成目标文件,而是通过:1$ g++ -S main.cpp

生成汇编文件main.s,不过需要给这个文件进行一下demangling,不然没法看:1$ cat main.s | c++filt

会输出(经过处理):1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20Object::Object():

pushq%rbp

movq%rsp, %rbp

movq%rdi, -8(%rbp)

nop

popq%rbp

ret

main:

pushq%rbp

movq%rsp, %rbp

subq$32, %rsp

movl%edi, -20(%rbp)

movq%rsi, -32(%rbp)

leaq-1(%rbp), %rax

movq%rax, %rdi

callObject::Object()

movl$0, %eax

leave

ret

还是看不到《对象模型》中所说的void Object::Object(Object * const)函数签名。不过注意到Object::Object():中有一句:1movq%rdi, -8(%rbp)

这句汇编的意思是把%rdi寄存器中的值赋予%rbp前移8个字节的内存地址中,即复制了64bit数据。

前面提到:1%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数...

那是不是有什么参数传入到了这个函数过程中了呢?不过我们现在暂时没有办法得知,需要后面提到的gdb帮助下才能验证。

强力工具gdb

本实验只用到gdb的一小部分功能,之前使用搜索引擎的时候,发现都可以用gdb调试多线程程序了,命令也挺简单,可以进入某一线程中进行调试。

回到本实验上,使用gdb前编译目标文件:1$ g++ -g main.cpp

添加-g是为了让生成的目标文件带有调试信息。

执行:1$ gdb a.out

进入调试。首先使用:1$ (gdb) info functions

会看到输出(经过处理):1

2

3File main.cpp:

void Object::Object();

int main(int, char**);

还是看不到我们想要的void Object::Object(Object * const),但是使用了万能的:1$ (gdb) print Object::Object

看到了输出:1$1 = {void (Object * const)} 0x400588 <:object>

这里说明了经过输出美化的Object::Object()真正的函数签名是void Object::Object(Object * const),这个实验的基本目的就达到了。

上面提到生成的汇编码中有暗示传入参数到Object::Object()的疑点,现在就用步进模式运行程序:1$ (gdb) start

然后键入s或者step一步一步执行,进入了Object::Object()函数执行内部:1

2Object::Object (this=0x7fffffffddcf) at main.cpp:5

5{}

注意到这个this=0x7fffffffddcf,这个是什么变量的地址值?在这里执行:1$ (gdb) print &obj

可惜当前Object::Object()上下文不存在这个变量,只能键入s进行到下一步回到main()中执行,得到:1$2 = (Object *) 0x7fffffffddcf

证明了void Object::Object(Object * const)是需要传入一个Object * const参数作为执行的上下文的。到了这里,忘了在Object::Object()上下文中检查上面提到的寄存器们了,只能用s或者n执行完这次,然后再次start并进入到Object::Object()的上下文中了,此时执行:1$ (gdb) info registers

得到:1

2

3

4

5

6rax 0x7fffffffddcf140737488346575

rdi 0x7fffffffddcf140737488346575

rbp 0x7fffffffdda00x7fffffffdda0

rsp 0x7fffffffdda00x7fffffffdda0

好了,根据上面所说的rdi作为存储函数参数地址的寄存器,那么其中存储的0x7fffffffddcf跟&obj的输出值对上了,也证明了Object::Object()真正的函数签名是void Object::Object(Object * const)。

证明完了之后,上面还特意贴出了rax寄存器中的值,也是0x7fffffffddcf,上面也提到:1%rax 作为函数返回值使用。

那么,最终Object::Object真正函数签名是Object* Object::Object(Object * const)呢?是的,然而这个函数是返回void,只要我们使用着这个编译器,我们便无法从其提供的语法层面获得这个返回值。

这个简单的实验,也从侧面说明了C++这个语言是有多复杂,编译器做了多少的小动作,不花点心思和用点工具,使用者只能看着编译出来的黑箱运行,用点printf或者cout看看是否正确运行。

至此,这个寻找类构造函数真正的签名的实验到此为止。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值