前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[linux][elf]ELF格式分析与常用工具以及不常见问题

[linux][elf]ELF格式分析与常用工具以及不常见问题

作者头像
皮振伟
发布2018-04-09 11:13:53
2K0
发布2018-04-09 11:13:53
举报
文章被收录于专栏:皮振伟的专栏皮振伟的专栏

前言: ELF只linux是最常用的可执行文件格式,这里简单分析一下ELF格式,介绍几个常用命令,并分享几个不常见的问题。 分析: 1,dynamically link 老习惯,从“Hello World”开始。

使用gcc采用默认参数编译,使用file命令查看: ELF 64-bit LSB executable:可见,是ELF格式64bit程序。那么在64位机上是否可以编译出来32bit程序呢?答案是可以的。但是gcc的参数需要多加一个-m32。 dynamically linked:链接方式是动态链接。说明程序在执行之前还需要链接其他的so。查看具体需要链接那些so,可以使用ldd hello查看。同时可见,gcc会默认使用动态链接。 interpreter /lib64/ld-linux-x86-64.so.2:说明程序使用的链接器是/lib64/ld-linux-x86-64.so.2。在用户程序执行之前,需要先用解释器来解释。 not stripped:没有strip过。一般编译的时候,会带着-g选项,可以用来debug。那么会在binary中带有debug symbol。如果使用了strip命令,就会剥离debug symbol。好处是会让binary小很多,并且在binary中不会残留编译机器上的信息;坏处就是出了问题之后,不能debug。比较好的做法是,例如在Android上,会把没有strip过的binary放到一个目录下保存,把strip过的放到板子(或者手机)中。这样子就可以让板子中的磁盘使用率降低,出了问题之后,把coredump文件copy到debug symbol目录下,不影响debug。 2,statically link 同样还是Hello world代码:

使用gcc hello.c -o hello -static编译,再使用file命令查看: statically linked:链接方式是静态链接。此类型可以直接运行,不需要再依赖其他的链接库了。同时也可以看到已经没有interpreter了。 3,*.so 想要编译so,可以给gcc加上-shared选项即可。 4,*.a 想要编译静态链接库,需要先用gcc编译成*.o文件,把*.o文件使用ar命令打包即可。 5,section

ELF格式是由一个或者多个section组成的。使用命令objdump -h hello,可见hello程序的所有section header。 如果要dump出来所有的内容,需要使用命令objdump -D hello .interp就是存放interpreter的地方; .init这个section会比较特殊,存放的代码会在main函数之前执行。这里要说明一点,尽管c语言默认main函数是入口函数,然后ELF还是有办法让代码在main函数之前执行; .rodata就是传说中的“静态常量区”; .bss&.data就是传说中的“全局变量区”,差别在于是否初始化。 objdump是一个比较少用又比较好用的命令。有时候,需要分析汇编代码,这个时候,objdump就该闪亮登场了。 6,interpreter linux-4.0.4/fs/binfmt_elf.c是kernel准备/开始运行ELF的地方,在load_elf_binary函数中:

可见,ELF在解析完binary的section之后,会尽量运行interpreter。所以,binary中使用的interpreter就可以有很大的操作空间,可以让程序共享一部分代码(就是各个so),在运行前进行链接。注意,在pc上,一般是使用ld;而在Android上,使用Google自己实现的linker。 7,LD_LIBRARY_PATH 这个环境变量用来告诉ld可以去哪些目录下搜索so。 很多情况下,未必把so防止到默认的/lib或者/lib64目录下,尤其是单独的用户项目。例如作者的示例代码编译出来的so,不想放到“系统”目录下,就会放到/home/hello/目录下,那么在启动程序之前,需要在shell中输入命令:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/hello就行,否则就会报错说找不到so。 说起这个问题,作者曾经在Android上,遇到过linker的bug,就是LD_LIBRARY_PATH环境变量的处理有问题。 8,LD_PRELOAD 如果多个so有相同的symbol,LD_PRELOAD目录下的so中的symbol会被优先使用。 注意,这个环境变量很有用,同时也会带来安全问题。例如,比较用户输入的密码使用strcmp函数,那么在LD_PRELOAD的so中实现了strcmp函数,就可以劫持用户输入的密码了。 再来一例,直接升级glibc是比较危险的,可能会让系统启动不起来。那么可以使用LD_PRELOAD导入新的glibc,验证没有问题之后,再升级。 9,cpp constructor & destructor

如示例代码,会是怎样的打印顺序? 如果使用gdb在main函数处设定断点,还是可以看到打印。如果在析构函数中设置断点,发现bt中并没有main。那么原因是什么呢?

先使用objdump -D hello得到上图的反汇编。

使用命令:c++filt _ZN5HelloD1Ev可以看到cpp的symbol名称,即Hello的析构函数;同理_ZN5HelloC1Ev是Hello::Hello()。 先看例子中的4008ad,调用了构造函数Hello::Hello()。 注意例子中的40090a这个地址,就是Hello::~Hello()的起始地址。在4008bc这个地址上,把40090a放到了edi中,在x86上,edi就是第一个参数的参数的地址,然后调用了__cxa_atexit函数,可见,g++编译的时候,已经帮我们把全局类变量的析构函数注册给了atexit。 这里看似是一个小技巧,可以在main函数之前和之后执行代码,实际使用起来,往往带来的负面效果更多。例如,如果声明了两个不同的类的全局变量,那么哪个先执行呢?如果声明 了同一个类的两个实例,哪个实例先构造呢?聪明的你可能会发现,先声明的实例先执行构造,那么如果在不同的.h文件中声明的呢?怎么确定哪个先被引用到的? 作者曾经在Android上遇到过一个bug:因为多个instance析构的顺序有问题而引起segmentation fault。这可是google大神写的binder的代码呦。 以作者的经验来看,绝大多数情况下,在main的第一行执行,或者在main函数之前执行,差别并不大。而且,atexit是把函数注册到一个table上,那么这个table会不会overflow呢,而且,可以尝试一下,直接使用syscall退出进程,看看是不是还会有这里的打印呢?总之,这里最好注意,轻易不要声明全局类型的instance!!! 10,weak symbol 同名symbol冲突的时候,可以使用weak symbol解决。 11,constructor 在编译的时候,指定某些函数的attribute是constructor,那么也可以让函数在main之前执行。 后面分析catchseg的时候,再具体分析源代码。 12,tools 另外,还有相关的工具,如addr2line,nm,objcopy。。。 后记: ELF遇到的问题,挺多时候都挺痛苦的。good luck~

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-02-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 AlwaysGeek 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档