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 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值。
实
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
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
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看看是否正确运行。
至此,这个寻找类构造函数真正的签名的实验到此为止。