本博文的主要内容是: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. const int g_A = 10;
  3. int a = 20;
  4. static int g_C = 30;
  5. int g_D;
  6. int main(void)
  7. {
  8. static int a = 40;
  9. register int b = 50;
  10. printf("Welcome%d\n", b);
  11. return 0;
  12. }
  13. 用gcc编译后,通过readelf工具查看可执行文件。
  14. gcc -g var.c -o var
  15. readelf -a var
  16. 截取我们所关心的几行:
  17. Section Headers:
  18. [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  19. ...
  20. [14] .text             PROGBITS        08048390 000390 0001bc 00  AX  0   0 16
  21. ...
  22. [16] .rodata           PROGBITS        08048568 000568 00001c 00   A  0   0  4
  23. ...
  24. [24] .data             PROGBITS        0804a010 001010 000014 00  WA  0   0  4
  25. [25] .bss              NOBITS          0804a024 001024 00000c 00  WA  0   0  4
  26. Symbol table '.symtab' contains 79 entries:
  27. Num:    Value  Size Type    Bind   Vis      Ndx Name
  28. ...
  29. 49: 0804a01c     4 OBJECT  LOCAL  DEFAULT   24 g_C
  30. 50: 0804a020     4 OBJECT  LOCAL  DEFAULT   24 a.1706
  31. 60: 0804a02c     4 OBJECT  GLOBAL DEFAULT   25 g_D
  32. 66: 0804a018     4 OBJECT  GLOBAL DEFAULT   24 a
  33. 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. const int g_A = 10;
  3. int a = 20;
  4. static int g_C = 30;
  5. int g_D;
  6. int main(void)
  7. {
  8. static int a = 40;
  9. register int c = 50;
  10. char b[] = "Hello world";
  11. printf("Welcome%d\n", c);
  12. return 0;
  13. }
  14. 我们通过objdump -dS var查看程序对应的汇编代码:
  15. 08048444 <main>:
  16. int a = 20;
  17. static int g_C = 30;
  18. int g_D;
  19. int main(void)
  20. {
  21. 8048444:     55                        push   %ebp
  22. 8048445:     89 e5                     mov    %esp,%ebp
  23. 8048447:     83 e4 f0                  and    $0xfffffff0,%esp
  24. 804844a:     53                        push   %ebx
  25. 804844b:     83 ec 2c                  sub    $0x2c,%esp
  26. 804844e:     65 a1 14 00 00 00         mov    %gs:0x14,%eax
  27. 8048454:     89 44 24 1c               mov    %eax,0x1c(%esp)
  28. 8048458:     31 c0                     xor    %eax,%eax
  29. static int a = 40;
  30. char b[] = "Hello World";
  31. 804845a:     c7 44 24 10 48 65 6c      movl   $0x6c6c6548,0x10(%esp)
  32. 8048461:     6c
  33. 8048462:     c7 44 24 14 6f 20 57      movl   $0x6f57206f,0x14(%esp)
  34. 8048469:     6f
  35. 804846a:     c7 44 24 18 72 6c 64      movl   $0x646c72,0x18(%esp)
  36. 8048471:     00
  37. register int c = 50;
  38. 8048472:     bb 32 00 00 00            mov    $0x32,%ebx
  39. printf("Welcome %d\n", c);
  40. 8048477:     b8 74 85 04 08            mov    $0x8048574,%eax
  41. 804847c:     89 5c 24 04               mov    %ebx,0x4(%esp)
  42. 8048480:     89 04 24                  mov    %eax,(%esp)
  43. 8048483:     e8 dc fe ff ff            call   8048364 <printf@plt>
  44. return 0;
  45. 8048488:     b8 00 00 00 00            mov    $0x0,%eax
  46. }
可见,给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的更多相关文章

  1. Linux开发工具的使用

    1.   Linux开发工具的使用 Vim编译的使用 Gdb调试工具的使用 Makefile的编写 linux跟踪调试 SSH的使用 subversion的使用 1.   Linux开发工具的使用 V ...

  2. Linux开发工具教程

    今天把上个星期写的Linux开发工具相关的教程整理一下,方便阅读: 1.第一课 GCC入门: 2.第二课 GCC入门之静态库以及共享库: 3.第三课 Makefile文件的制作(上) : 4.第四课 ...

  3. Linux开发工具之Makefile(上)

    二.makefile(上) 01.make工具   利用make工具可以自动完成编译工作.这些工作包括:如果修改了某几 个源文件,则只重装新编译这几个源文件:如果某个头文件被修改了,则 重新编译所有包 ...

  4. 蜂鸟E203系列——Linux开发工具

    欲观原文,请君移步 Vivado安装 vivado是运行工程的工具,所以必须安装 后台回复[vivado2017]可获取vivado 2017.4 | 后台回复[vivado2020]可获取vitis ...

  5. [转帖]FPGA开发工具汇总

    原帖:http://blog.chinaaet.com/yocan/p/5100017074 ----------------------------------------------------- ...

  6. Linux开发工具之gcc

    一.gcc入门(上)   1.gcc相关概念   gcc(GNU C Compiler)编译器,最初支持C语言,现已支持C.C++.Java.Pascal.Ada.COBOL语言等:支持多种硬件平台: ...

  7. Linux开发工具之gdb(下)

    三.gdb调试(下) 01.查看运行时数据 print - 查看变量值 ptype - 查看类型 print array - 查看数组 print *array@len - 查看动态内存 print ...

  8. Linux开发工具之gdb(上)

    三.gdb调试(上) 01.gdb:gdb是GNU debugger的缩写,是编程调试工作. 功能:   启动程序,可以按照用户自定义的要求随心所欲的运行程序:   可让被调试的程序在用户所指定的调试 ...

  9. Linux开发工具之Makefile(下)

    二.Makefile(下) 01.make常用内嵌函数 函数调用   $(function arguments) $(wildcard PATTERN)   当前目录下匹配模式的文件   例如:src ...

  10. Linux开发工具_yum使用

    yum 的说明与使用 1.什么是yum? 软件包管理器 提供了查找.安装.删除某一个.一组甚至全部软件的命令 命令简洁好用 2.yum语法 yum [ 选项 ] [命令] [安装包] 选项: -h h ...

随机推荐

  1. 文心一言 VS 讯飞星火 VS chatgpt (55)-- 算法导论6.3 1题

    文心一言 VS 讯飞星火 VS chatgpt (55)-- 算法导论6.3 1题 一.参照图6-3 的方法,说明 BUILD-MAX-HEAP在数组 A=(5,3,17,10,84,19,6,22, ...

  2. 最基本的SpringCloud的搭建

    对于springcloud而言,模块是按业务进行区分的: 父工程 依赖 <parent> <groupId>org.springframework.boot</group ...

  3. 智能对联模型太难完成?华为云ModelArts助你实现!手把手教学

    摘要:农历新年将至,听说华为云 AI 又将开启智能对对联迎接牛气冲天,让我们拭目以待!作为资深 Copy 攻城狮,想要自己实现一个对对联的模型,是不能可能完成的任务,因此我搜罗了不少前人的实践案例,今 ...

  4. 2022 IDC中国未来企业大奖优秀奖颁布,华为云数据库助力德邦快递获奖

    摘要:华为云数据库助力德邦快递打造的"基于数智融合的一站式物流供应链平台"项目从500多个项目中脱颖而出,荣获2022 IDC中国未来企业大奖优秀奖"未来智能领军者&qu ...

  5. 鸿蒙轻内核源码分析:Newlib C

    摘要:本文介绍了LiteOS-M内核Newlib C的实现,特别是文件系统和内存分配释放部分,最后介绍了Newlib钩子函数. 本文分享自华为云社区<鸿蒙轻内核M核源码分析系列二十 Newlib ...

  6. vue2升级vue3:Vue Router报错,directly inside <transition> or <keep-a

    vue3 报这个错误: vue-router.mjs:35 [Vue Router warn]: <router-view> can no longer be used directly ...

  7. 十大 CI/CD 安全风险(五)

    在本篇文章中,我们将了解第三方服务的监管不足,工件完整性验证及日志可见性不足这三个关键 CI/CD 安全风险,并给出缓解相应风险的建议与措施. 第三方服务监管不足 CI/CD 攻击面包括企业资产,例如 ...

  8. PPT 光效果

    点状.线状.面状.光影 "光" = PPT高大上的秘密

  9. logback.xml 配置文件

    logback.xml <?xml version="1.0" encoding="UTF-8"?> <configuration> & ...

  10. Hadoop面试题(一)

    1.集群的最主要瓶颈 磁盘IO 2.Hadoop运行模式 单机版.伪分布式模式.完全分布式模式 3.Hadoop生态圈的组件并做简要描述 1)Zookeeper:是一个开源的分布式应用程序协调服务,基 ...