【转帖】Linux开发工具 — readelf、objdump、hexdump

linux,开发工具,readelf,objdump,hexdump · 浏览次数 : 0

小编点评

#数组元素b[n]的地址 = 数组的基地址(b 做右值就表示这个基地址) + n ×  每个元素的字节数 #对于上例中的变量c,其汇编代码如下: register int c = 50; 8048472:     bb 32 00 00 00 mov    $0x32,%ebx 变量c 并没有在栈上分配存储空间,而是直接存在ebx 寄存器里,后面调用printf 也是直接从ebx寄 存器里取出c 的值当参数压栈,这就是register关键字的作用,指示编译器尽可能分配一个寄存器来存储这个变量。

正文

本博文的主要内容是:1)readelf工具查看ELF文件的信息;2)hexdump工具查看这块内存;3)objdump工具对文件进行反汇编。
前一段时间对Linux不熟,所以很多命令不知道。学习C时候需要偶尔看一下汇编用来理解。我喜欢用问题的形式来学习和总结。

1. 如何看一个程序代码变量的存储布局?    
     这个问题在查看C代码中的一些关键字的作用很有效。如:const、static、extern等。readelf这个工具就派上用场了。
(1)readelf工具
          a. 作用:用来显示ELF文件的信息。
          b. 使用:readelf <option> <file>,其中我们常用选项有-a,<file>可以是目标文件或可执行文件。
          (想了解该工具详细的信息,请通过Linux下的man命令查一下。)
     下面通过例子来使用一下这个工具:
  1. int main(void)  
  2. {  
  3.          printf("hello, welcome!\n");  
  4.          return 0;  
  5. }  
 用gcc编译后,通过readelf工具查看可执行文件。
  1. gcc -g hello.c -o hello  
  2.     readelf -a hello  
  3.     我们会看到:  
  4. LF Header:  
  5.     Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00  
  6.     Class:                             ELF32  
  7.     Data:                              2's complement, little endian  
  8.     Version:                           1 (current)  
  9.     OS/ABI:                            UNIX - System V  
  10.     ABI Version:                       0  
  11.     Type:                              EXEC (Executable file)  
  12.     Machine:                           Intel 80386  
  13.     Version:                           0x1  
  14.     Entry point address:               0x8048330  
  15.     Start of program headers:          52 (bytes into file)  
  16.     Start of section headers:          5052 (bytes into file)  
  17.     Flags:                             0x0  
  18.     Size of this header:               52 (bytes)  
  19.     Size of program headers:           32 (bytes)  
  20.     Number of program headers:         8  
  21.     Size of section headers:           40 (bytes)  
  22.     Number of section headers:         38  
  23.     Section header string table index: 35  

 ELF Header描述了操作系统为UNIX,PC机体系结构是Intel 80386。Section Header Table中有38个Section Header ,
从文件地址5052开始,每个Section Header 占40字节,共320 字节,到文件地址0x207 结束。文件地址是这样定义的:
文件开头第一个字节的地址是0x8048330。
  1. Section Headers:  
  2.      [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al  
  3.      [ 0]                   NULL            00000000 000000 000000 00      0   0  0  
  4.      ...  
  5.      [10] .rel.dyn          REL             08048298 000298 000008 08   A  6   0  4  
  6.      [11] .rel.plt          REL             080482a0 0002a0 000018 08   A  6  13  4  
  7.      [12] .init             PROGBITS        080482b8 0002b8 000030 00  AX  0   0  4  
  8.      [13] .plt              PROGBITS        080482e8 0002e8 000040 04  AX  0   0  4  
  9.      [14] .text             PROGBITS        08048330 000330 00016c 00  AX  0   0 16  
  10.      [15] .fini             PROGBITS        0804849c 00049c 00001c 00  AX  0   0  4  
  11.      [16] .rodata           PROGBITS        080484b8 0004b8 000018 00   A  0   0  4  
  12.      ...  
  13.      [22] .got              PROGBITS        08049ff0 000ff0 000004 04  WA  0   0  4  
  14.      [23] .got.plt          PROGBITS        08049ff4 000ff4 000018 04  WA  0   0  4  
  15.      [24] .data             PROGBITS        0804a00c 00100c 000008 00  WA  0   0  4  
  16.      [25] .bss              NOBITS          0804a014 001014 000008 00  WA  0   0  4  
  17.      [26] .comment          PROGBITS        00000000 001014 000023 01  MS  0   0  1  
  18.      [27] .debug_aranges    PROGBITS        00000000 001037 000020 00      0   0  1  
  19.      ...  
  20.      [35] .shstrtab         STRTAB          00000000 001264 000156 00      0   0  1  
  21.      [36] .symtab           SYMTAB          00000000 0019ac 000490 10     37  53  4  
  22.      [37] .strtab           STRTAB          00000000 001e3c 0001fb 00      0   0  1  

     从Section Header 中读出各Section的描述信息,这里有38个SectionHeader。其中,我们关心的有.text,.rodata,.data,.bss。
它们分别是代码段,常量区,数据段,未初始化数据段。Addr是这些段加载到内存中的地址(都是虚拟地址),加载地址要在链接时填写,
现在空缺,所以是全0 。Off 和Size两列指出了各Section的文件地址,比如.data段从文件地址00100c开始,一共0x08个字节,根据以上信息可以描绘出
整个文件的布局。
(2)查看一个程序代码变量的存储布局
     先看例子:
  1. #include <stdio.h>  
  2.       
  3.      const int g_A = 10;  
  4.      int a = 20;  
  5.      static int g_C = 30;  
  6.      int g_D;  
  7.   
  8.      int main(void)  
  9.      {  
  10.           static int a = 40;  
  11.           register int b = 50;  
  12.   
  13.           printf("Welcome%d\n", b);  
  14.   
  15.           return 0;  
  16.      }  
  17.      用gcc编译后,通过readelf工具查看可执行文件。  
  18.      gcc -g var.c -o var  
  19.      readelf -a var  
  20.      截取我们所关心的几行:  
  21. Section Headers:  
  22.      [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al  
  23.      ...  
  24.      [14] .text             PROGBITS        08048390 000390 0001bc 00  AX  0   0 16  
  25.      ...  
  26.      [16] .rodata           PROGBITS        08048568 000568 00001c 00   A  0   0  4  
  27.      ...  
  28.      [24] .data             PROGBITS        0804a010 001010 000014 00  WA  0   0  4  
  29.      [25] .bss              NOBITS          0804a024 001024 00000c 00  WA  0   0  4  
  30.   
  31. Symbol table '.symtab' contains 79 entries:  
  32.      Num:    Value  Size Type    Bind   Vis      Ndx Name  
  33.      ...    
  34.     49: 0804a01c     4 OBJECT  LOCAL  DEFAULT   24 g_C  
  35.     50: 0804a020     4 OBJECT  LOCAL  DEFAULT   24 a.1706  
  36.     60: 0804a02c     4 OBJECT  GLOBAL DEFAULT   25 g_D  
  37.     66: 0804a018     4 OBJECT  GLOBAL DEFAULT   24 a  
  38.     75: 08048570     4 OBJECT  GLOBAL DEFAULT   16 g_A  

 下面分析各个变量存储的范围:
     g_A:
     变量g_A用const修饰,表示g_A是只读的,不可修改,它被分配的地址是0x08048570 ,从readelf的输出的Section Headers可以看到这个地址位于.rodata段。
它在文件中的地址是0x568~0x584. 我们也可以通过hexdump命令查看这块内存的内容:hexdump -C var
00000570  0a 00 00 00 57 65 6c 63  6f 6d 65 20 25 64 0a 00  |....Welcome %d..|
我们看到,0x570地址存的就是0a 00 00 00。也即十进制的10.我们还看到程序中的字符串字面值"Hello world!\n"分配在.rodata段的末尾。
字符串字面值是只读的,相当于在全局作用域定义了一个const数组:
const char helloworld[] = {'W', 'e', 'l', 'c', 'o', 'm', 'e', '%', 'd', '\n', '\0'};
程序加载运行时,.rodata段和.text段通常合并到一个Segment中,操作系统将这个Segment的页面只读保护起来,防止意外的改写。
     注意,像A 这种const变量在定义时必须初始化。因为只有初始化时才有机会给它一个值,一旦定义之后就不能再改写了,也就是不能再赋值了。
从上面readelf的输出可以看到.data段从地址0x0804a010开始,长度是0x14,也就是到地址0x804a024 结束。在.data段中有三个变量a ,g_C和a.1706。
     a:
     a是一个GLOBAL的符号,而g_C被static关键字修饰了,导致它成为一个LOCAL的符号,所以static 在这里的作用是声明b这个符号为LOCAL的,不被链接器处理。
还有一个a.1706是什么呢?它就是main函数中的static int a。函数中的static变量不同于局部变量,它并不是在调用函数时分配,在函数返回时释放,
而是像全局变量一样静态分配,所以用“ static” (静态)这个词。另一方面,函数中的static 变量的作用域和以前讲的局部变量一样,只在函数中起作用,比如main函数中的a 这个变量名只在main函数中起作用,在别的函数中
说变量a 就不是指它了,所以编译器给它的符号名加了一个后缀,变成a.1589 ,以便和全局变量a 以及其它函数的变量a 区分开。
     g_D:
     .bss段从地址0x804a024开始(紧挨着.data段),长度为0xc,也就是到地址0x804a030结束。变量g_D位于这个段。因为g_D未初始化。
     局部变量c和数组b在下一个知识点说明。
     变量c 并没有在栈上分配存储空间,而是直接存在eax寄存器里,后面调用printf也是直接从eax寄
存器里取出c 的值当参数压栈,这就是register 关键字的作用,指示编译器尽可能分配一个寄存器
来存储这个变量。我们还看到调用printf 时对于"Hello world %d\n" 这个参数压栈的是它
在.rodata段中的首地址,而不是把整个字符串压栈,所以在第 4 节 “ 字符串” 中说过,字符串在使
用时可以看作数组名,如果做右值则表示数组首元素的地址(或者说指向数组首元素的指针),我
们以后讲指针还要继续讨论这个问题。
    
2. 如何反汇编?
     我们在理解某些C语言代码时,有时候要深入理解,就必须跑到汇编去理解。所以反汇编也很重要,使用的命令:objdump -dS <file>
     我们给上面的代码加一个数组b,修改后的代码如下:
  1. #include <stdio.h>  
  2.       
  3.      const int g_A = 10;  
  4.      int a = 20;  
  5.      static int g_C = 30;  
  6.      int g_D;  
  7.   
  8.      int main(void)  
  9.      {  
  10.           static int a = 40;  
  11.           register int c = 50;  
  12.           char b[] = "Hello world";  
  13.           printf("Welcome%d\n", c);  
  14.   
  15.           return 0;  
  16.      }  
  17.      我们通过objdump -dS var查看程序对应的汇编代码:  
  18.      08048444 <main>:  
  19.      int a = 20;  
  20.      static int g_C = 30;  
  21.      int g_D;  
  22.   
  23.      int main(void)  
  24.      {  
  25.      8048444:     55                        push   %ebp  
  26.      8048445:     89 e5                     mov    %esp,%ebp  
  27.      8048447:     83 e4 f0                  and    $0xfffffff0,%esp  
  28.      804844a:     53                        push   %ebx  
  29.      804844b:     83 ec 2c                  sub    $0x2c,%esp  
  30.      804844e:     65 a1 14 00 00 00         mov    %gs:0x14,%eax  
  31.      8048454:     89 44 24 1c               mov    %eax,0x1c(%esp)  
  32.      8048458:     31 c0                     xor    %eax,%eax  
  33.           static int a = 40;  
  34.           char b[] = "Hello World";  
  35.      804845a:     c7 44 24 10 48 65 6c      movl   $0x6c6c6548,0x10(%esp)  
  36.      8048461:     6c  
  37.      8048462:     c7 44 24 14 6f 20 57      movl   $0x6f57206f,0x14(%esp)  
  38.      8048469:     6f  
  39.      804846a:     c7 44 24 18 72 6c 64      movl   $0x646c72,0x18(%esp)  
  40.      8048471:     00  
  41.           register int c = 50;  
  42.      8048472:     bb 32 00 00 00            mov    $0x32,%ebx  
  43.   
  44.           printf("Welcome %d\n", c);  
  45.      8048477:     b8 74 85 04 08            mov    $0x8048574,%eax  
  46.      804847c:     89 5c 24 04               mov    %ebx,0x4(%esp)  
  47.      8048480:     89 04 24                  mov    %eax,(%esp)  
  48.      8048483:     e8 dc fe ff ff            call   8048364 <printf@plt>  
  49.   
  50.           return 0;  
  51.      8048488:     b8 00 00 00 00            mov    $0x0,%eax  
  52.      }      

可见,给b 初始化用的这个字符串"Hello world" 并没有分配在.rodata段,而是直接写在指令里了,
通过三条movl指令把12个字节写到栈上,这就是b 的存储空间。
 
 
可以看出:虽然栈是从高地址向低地址增长的,但 数组总是从低地址向高地址排列的,按从低地址到高
地址的顺序依次是b[0]、b[1]、b[2] ……
     数组元素b[n]的地址 = 数组的基地址(b 做右值就表示这个基地址) + n ×  每个元素的字节数
当n=0 时,元素b[0]的地址就是数组的基地址,因此数组下标要从0 开始而不是从1 开始。

     对于上例中的变量c,其汇编代码如下:
     register int c = 50;
     8048472:     bb 32 00 00 00            mov    $0x32,%ebx
     变量c 并没有在栈上分配存储空间,而是直接存在ebx 寄存器里,后面调用printf 也是直接从ebx寄
存器里取出c 的值当参数压栈,这就是register关键字的作用,指示编译器尽可能分配一个寄存器来存储这个变量。
文章知识点与官方知识档案匹配,可进一步学习相关知识
CS入门技能树Linux入门初识Linux32215 人正在系统学习中

与【转帖】Linux开发工具 — readelf、objdump、hexdump相似的内容:

【转帖】Linux开发工具 — readelf、objdump、hexdump

本博文的主要内容是:1)readelf工具查看ELF文件的信息;2)hexdump工具查看这块内存;3)objdump工具对文件进行反汇编。 前一段时间对Linux不熟,所以很多命令不知道。学习C时候需要偶尔看一下汇编用来理解。我喜欢用问题的形式来学习和总结。 1. 如何看一个程序代码变量的存储布局

[转帖]Linux开发环境——SCL软件集

一、SCL简介 1、SCL简介 SCL(Software Collections)是一个CentOS/RHEL Linux平台的软件多版本共存解决方案,为RHEL/CentOS Linux用户提供一种方便、安全地安装和使用应用程序和运行时环境的多个版本的方式,同时避免把系统搞乱。 CentOS/RH

[转帖]linux后台开发必知的io优化知识总结

系统学习 IO性能对于一个系统的影响是至关重要的。一个系统经过多项优化以后,瓶颈往往落在数据库;而数据库经过多种优化以后,瓶颈最终会落到IO。而IO性能的发展,明显落后于CPU的发展。Memchached也好,NoSql也好,这些流行技术的背后都在直接或者间接地回避IO瓶颈,从而提高系统性能。 IO

[转帖]linux后台开发必知的io优化知识总结

系统学习 IO性能对于一个系统的影响是至关重要的。一个系统经过多项优化以后,瓶颈往往落在数据库;而数据库经过多种优化以后,瓶颈最终会落到IO。而IO性能的发展,明显落后于CPU的发展。Memchached也好,NoSql也好,这些流行技术的背后都在直接或者间接地回避IO瓶颈,从而提高系统性能。 IO

[转帖]Linux句柄调优之nofile、nr_open、file-max

https://www.jianshu.com/p/8fb056e7b9f8 在开发运维的时候我们常常会遇到类似“Socket/File: Can’t open so many files”,“无法打开更多进程”,或是coredump过大等问题,这些都可以设置资源限制来解决。今天在教某位客户设置最大

[转帖]Linux句柄调优之nofile、nr_open、file-max

https://www.jianshu.com/p/8fb056e7b9f8 在开发运维的时候我们常常会遇到类似“Socket/File: Can’t open so many files”,“无法打开更多进程”,或是coredump过大等问题,这些都可以设置资源限制来解决。今天在教某位客户设置最大

[转帖]Linux句柄调优之nofile、nr_open、file-max

https://www.jianshu.com/p/8fb056e7b9f8 在开发运维的时候我们常常会遇到类似“Socket/File: Can’t open so many files”,“无法打开更多进程”,或是coredump过大等问题,这些都可以设置资源限制来解决。今天在教某位客户设置最大

[转帖] Linux命令拾遗-常用的辅助开发类命令

https://www.cnblogs.com/codelogs/p/16060735.html 原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。 简介# 之前介绍了大量的命令,有基础操作相关的,也有问题诊断相关的,但Linux中还有一类命令,被时常使用在工作当中,如c

[转帖]Linux—vi/vim批量注释及取消注释

https://www.jianshu.com/p/45c252e9d2b6 应用场景 在开发场景中,经常遇到对一些配置文件进行注释,当然如果确定不用的时候我们可以通过vim中的D命令进行删除。当需要对配置文件中的一些配置进行批量注释#的时候,我们可以通过下面的方式进行。 语法格式 批量注释 :fr

[转帖]Linux中iptraf命令详解

https://www.sohu.com/a/217620611_610671 iptraf是一个基于ncurses开发的IP局域网监控工具,它可以实时地监视网卡流量,可以生成各种网络统计数据,包括TCP信息、UDP统计、ICMP和OSPF信息、以太网负载信息、节点统计、IP校验和错误和其它一些信息