重学计算机(二、反汇编和hello_world.o分析)

之前的计划就是想重新学习一边编译链接的过程,这次是安排上了,但是还是感觉太年轻了,以上一篇的问题,去准备这一篇的时候,发现学习的东西还是很多的,应该是很多很多,惆怅。

但是flag已经发出去了,就硬着头皮去搞把,希望能坚持下来。加油。。。

2.1 目标文件

我们按照上一节的步骤,我们分步编译出来了,预处理文件.i,还有汇编文件.s,还有目标文件.o。

预处理文件上一篇就讲过了,汇编文件的话里面都是一些汇编语句,这个以后再讲,这一节我们的目标就是.o文件。

我有在之前的hello_world的基础上,添加了一些变量,和一个函数调用,这样分析起来会更全面。

#include <stdio.h>

int g_a = 0;
int g_b = 84;

int func1(int i)
{
    printf("i = %d\n", i);
    return 0;
}

int main(int argc, char **argv)
{
    static int s_a = 0;
    static int s_b = 84;

    int a = 1;
    int b;
    func1(s_a+s_b+a+b);
    printf("hello world %d %d %d\n", g_a, a, b);

    return 0;
}

在这里hello_world中,我们定义了全局变量,局部变量,函数调用。

就按上一节的操作,我们直接汇编成.o文件。

我们可以使用file命令查看这个文件的格式:

root@ubuntu:~/c_test/02# file hello_world.o 
hello_world.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

很明显hello_world.o文件也是一个ELF文件,不过不是可执行文件,是可重定位的文件,等待链接器把多个.o文件链接起来,怎么链接呢?下节再讲。

2.2 objdump命令

linux下,已经准备了两个命令给我们,就是给我们这些不安分的,一个是objdump反汇编的命令,一个是readelf解析elf的命令,这样我们就不用一个字节一个字节去对比elf文件,如果有兴趣的话,也可以去一个字节一个字节对比,不过我这样就不对比了,后来的任务巨重。

下面简单介绍一个objdump常用的参数:

objdump -f hello_world.o		# 显示文件的头信息
objdump -h hello_world.o		# 显示常用段信息
objdump -x hello_world.o		# 显示全面的段信息
objdump -d hello_world.o		# 显示机制指令段的汇编信息 很常用
objdump -D hello_world.o		# 显示全部汇编信息
objdump -s hello_world.o	    # 显示请求的所有部分的全部内容,以十六进制的方式
objdump -t hello_world.o        # 显示符号表,类似nm -s

2.3 readelf命令

接着看看readelf常用的参数:

readelf -h hello_world.o		# 显示elf格式的头信息
readelf -S hello_world.o		# 显示elf段表
readelf -s hello_world.o		# 显示符号表
readelf -r hello_world.o		# 显示重定位
readelf -d hello_world.o		# 显示动态段

其实也不是很熟这些命令,以后有常用在添加。

2.4 hello_world.o分析

来到了这一章的重点,我们先分析hello_world.o,下次分析hello_world,以后看安排要不要把.a .so的也分析一波。

2.4.1 头信息

查看头信息,两个命令都可以查看到:

2.4.1.1 objdump -f
root@ubuntu:~/c_test/02# objdump -f hello_world.o

hello_world.o:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000

可以查看到编译文件的系统架构,文件的格式,后面还有文件的标记:

/* BFD contains relocation entries.  */
#define HAS_RELOC      0x01

  /* BFD is directly executable.  */
#define EXEC_P         0x02
...
  /* BFD has symbols.  */
#define HAS_SYMS       0x10
2.4.1.2 readelf -h
root@ubuntu:~/c_test/02# readelf -h hello_world.o 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1168 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 10
root@ubuntu:~/c_test/02# 

这个readelf命令重点是根据elf的二进制编码,然后把属于头部分的信息,转码出来,所以我们看的就是这个样子,又回想起当年,动不动就分析二进制的时候,哎,现在有一个工具多爽,直接输入一个命令就出来了,就不用去对这二进制数据分析了。

2.4.2 段信息

段信息是一个很重要的知识点,段信息也是可以使用两个命令来读取的。

这里readelf -S这个我们留着下节去分析hello_world,这一节我们用objdump -h。

先来总览一下:

root@ubuntu:~/c_test/02# size hello_world.o
   text	   data	    bss	    dec	    hex	filename
    245	      8	      8	    261	    105	hello_world.o

我们可以用size来简单查看各个段大小,text段、data段、bss段。

root@ubuntu:~/c_test/02# objdump -h hello_world.o

hello_world.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000007f  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  000000c0  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000008  0000000000000000  0000000000000000  000000c8  2**2
                  ALLOC
  3 .rodata       0000001e  0000000000000000  0000000000000000  000000c8  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      00000036  0000000000000000  0000000000000000  000000e6  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  0000011c  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000058  0000000000000000  0000000000000000  00000120  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

重点出来了,接下来的目标,我们就重点分析这几个段。

2.4.2.1 text段

这一段是专门存放代码的,我们就来看看代码最后是怎么存的?想看不?

看的话还是需要objdump这个老工具人

root@ubuntu:~/c_test/02# objdump -s hello_world.o 

hello_world.o:     file format elf64-x86-64

Contents of section .text:
 0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E...
 0010 bf000000 00b80000 0000e800 000000b8  ................
 0020 00000000 c9c35548 89e54883 ec20897d  ......UH..H.. .}
 0030 ec488975 e0c745f8 01000000 8b150000  .H.u..E.........
 0040 00008b05 00000000 01c28b45 f801c28b  ...........E....
 0050 45fc01d0 89c7e800 0000008b 05000000  E...............
 0060 008b4dfc 8b55f889 c6bf0000 0000b800  ..M..U..........
 0070 000000e8 00000000 b8000000 00c9c3    ............... 
... 只留下 .text 段     
root@ubuntu:~/c_test/02# 

-s 是把所有段以十六进制的格式输出,这也是代码中存储的格式,直接看十六进制看不懂,是不是,所以我们还需要把机器指令段反汇编出来,一对比就明白了很多。

root@ubuntu:~/c_test/02# objdump -d hello_world.o 

hello_world.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <func1>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	89 7d fc             	mov    %edi,-0x4(%rbp)
   b:	8b 45 fc             	mov    -0x4(%rbp),%eax
   e:	89 c6                	mov    %eax,%esi
  10:	bf 00 00 00 00       	mov    $0x0,%edi
  15:	b8 00 00 00 00       	mov    $0x0,%eax
  1a:	e8 00 00 00 00       	callq  1f <func1+0x1f>
  1f:	b8 00 00 00 00       	mov    $0x0,%eax
  24:	c9                   	leaveq 
  25:	c3                   	retq   

0000000000000026 <main>:
  26:	55                   	push   %rbp
  27:	48 89 e5             	mov    %rsp,%rbp
  2a:	48 83 ec 20          	sub    $0x20,%rsp
  2e:	89 7d ec             	mov    %edi,-0x14(%rbp)
  31:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
  35:	c7 45 f8 01 00 00 00 	movl   $0x1,-0x8(%rbp)
  3c:	8b 15 00 00 00 00    	mov    0x0(%rip),%edx        # 42 <main+0x1c>
  42:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 48 <main+0x22>
  48:	01 c2                	add    %eax,%edx
  4a:	8b 45 f8             	mov    -0x8(%rbp),%eax
  4d:	01 c2                	add    %eax,%edx
  4f:	8b 45 fc             	mov    -0x4(%rbp),%eax
  52:	01 d0                	add    %edx,%eax
  54:	89 c7                	mov    %eax,%edi
  56:	e8 00 00 00 00       	callq  5b <main+0x35>
  5b:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 61 <main+0x3b>
  61:	8b 4d fc             	mov    -0x4(%rbp),%ecx
  64:	8b 55 f8             	mov    -0x8(%rbp),%edx
  67:	89 c6                	mov    %eax,%esi
  69:	bf 00 00 00 00       	mov    $0x0,%edi
  6e:	b8 00 00 00 00       	mov    $0x0,%eax
  73:	e8 00 00 00 00       	callq  78 <main+0x52>
  78:	b8 00 00 00 00       	mov    $0x0,%eax
  7d:	c9                   	leaveq 
  7e:	c3                   	retq   

左侧第一列,是这条语句的偏移,通过这个就能明白x86的机器命令是变长的。

左侧第二列,就是十六进制的内容,可以看成机器码。

右侧,就是对应的汇编指令了。

通过我们对比两个的十六进制,是不是完全符合,所以说代码存储在文件中,就是这样存储的。

2.4.2.2 .data段

程序中初始化的数据就是不为0的全局变量,不为0的局部静态变量

我们看上面的这个段是8个字节,怎么是8个字节呢?

在这里插入图片描述

这两个数据是存储在data段中的,不信是不是?

我们用反汇编来验证一波:

Contents of section .data:
 0000 54000000 54000000                    T...T...  

0x54000000 = 84,这里面就是存了两个84。

为什么最低位在前面,这个就是linux系统一般都是小端模式。

顺序的话,应该是按变量的加载顺序,取值。

2.4.2.3 BSS段

程序中为0的全局变量,为0的静态变量,或者是不赋值,不赋值也会默认为0

为什么会分出一个.bss段,是因为这样可以节省一点空间,等程序运行的时候在搞上来,所以这个段是需要大小的,统计程序中符合这个段的变量大小,留下空间,但是因为值为0,所以不要存储。

CONTENTS表示是这个.o程序包含CONTENTS这个字段,因为bss没有,所以没包含。

2.4.2.4 rodata

只读数据段,一般是放的是常量(const),比如字符串常量。

为什么会出现这样的只读数据段,这是在编译器中把这些只读的数据存储到一起,如果这段数据被修改,就会出现报错,编译器是这样检测只读数据的。

不过好像可以用一个指针指向这个地址,然后就可以直接修改了,因为linux系统,存放只读数据也是放在RAM上,理论上也是可以绕过编译器修改的。

像51单片机,因为RAM太少,只读数据是直接存储到flash中的,flash是一个不能改的存储介质,这个用指针指向也是不能修改的。

其实我很疑惑的一定就是这个代码中没有只读数据啊,怎么会有呢?

这就是我们c语言功底不够用,其实printf函数中的字符串,就是存储成字符串常量的,就是存在这个只读数据段的。不信?我们反汇编看看:

Contents of section .rodata:
 0000 69203d20 25640a00 68656c6c 6f20776f  i = %d..hello wo
 0010 726c6420 25642025 64202564 0a00      rld %d %d %d..  

是不是安排的明明白白,清清楚楚的。

2.4.2.5 comment

包含编译器的信息段

Contents of section .comment: 
 0000 00474343 3a202855 62756e74 7520352e  .GCC: (Ubuntu 5.
 0010 342e302d 36756275 6e747531 7e31362e  4.0-6ubuntu1~16.
 0020 30342e31 32292035 2e342e30 20323031  04.12) 5.4.0 201
 0030 36303630 3900                        60609.  
2.4.2.6 .note.GNU-stack

堆栈提示段,表示不是很明白,

2.4.2.7 .eh_frame

保存c++异常处理的内容

Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 26000000 00410e10 8602430d  ....&....A....C.
 0030 06610c07 08000000 1c000000 3c000000  .a..........<...
 0040 00000000 59000000 00410e10 8602430d  ....Y....A....C.
 0050 0602540c 07080000                    ..T.....

这一段也不懂,以后再补补,不懂的地方还有很多。

2.4.2.8 自定义段
__attribute__((section("FOO"))) int global = 42;

__attribute__((section("BAR"))) void foo()

{undefined

}

当初分析uboot代码的时候,就很多这种自定义段,uboot启动后,会把同一个初始化的函数放到同一个段,然后好像是依次编译这个段的内容。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值