程序编译链接运行深入剖析

 学C语言的第一个程序就是以下这个helloworld程序:

1
2
3
4
5
6
#include
int main()
{
     printf ( "hello, world!\n" );
}

它通过gcc -o hello hello.c编译,然后通过./hello就可以运行了,屏幕上打印出”hello, world\n”

不过它是如何被编译和链接的,又是如何在系统中运行的?第二次看《深入理解计算机系统》,终于明白了一点,所以做个总结。

编译

gcc -o hello hello.c这个命令其实gcc帮你干完了编译和链接两个工作。(编译其实还包括了汇编,可以把*.c->*.s理解为编译,.s->.o理解为汇编)

如果想只编译那么要用-c选项,gcc -c hello.c,这会生成一个hello.o文件,这是一个可重定位目标文件,有关可重定位目标文件,我在这篇文章里做过总结。

这个文件由一个ELF头、一个节头部表、以及很多个节组成。

比较重要的节有:

.text节,程序编译后的机器代码
.rodata节,只读数据,比如字符串常量、switch语句中的跳转表等就放在这个节
.data节,已初始化的全局变量
.bss节,未初始化的全局变更
.symtab节,一个符号表,存放程序中定义和引用的函数和全局变量的信息
.rel.text节和.rel.data节存放与重定位有关的信息
.debug节和.line节,只有用-g选项进行编译才会有
,存放与调试有关的信息
.strtab,一个字符串表。

先通过objdump -d -r hello.o对该文件进行反汇编:

00000000 <main>:
   0:	55                   	push   %ebp
   1:	89 e5                	mov    %esp,%ebp
   3:	83 e4 f0             	and    $0xfffffff0,%esp
   6:	83 ec 10             	sub    $0x10,%esp
   9:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
			c: R_386_32	.rodata
  10:	e8 fc ff ff ff       	call   11 <main+0x11>
			11: R_386_PC32	puts
  15:	c9                   	leave
  16:	c3                   	ret

-r选项同时显示出了代码重需要重定位的地方。这段程序其实就是将参数(字符常指针)入栈之后,调用打印函数。不过由于现在还不确定程序将来会在哪段地址运行,所以还没法确定字符串的地址、以及打印函数的地址,而这要到链接才能确定,到后面再讨论。

链接

使用gcc -o hello hello.o来对其进行链接。

链接包括符号解析和重定位两步。

符号解析就是将输入的可重定位目标文件的符号表中的符号与其定义联系起来。比如我们在程序中调用了printf函数,它在系统的一个动态库中定义。

完成符号解析之后,链接器就知道输入目标模块中的代码节和数据节的确切大小了。就可以开始重定位了,这个步骤中,将合并输入模块,并为每一个符号分配运行地址。

重定位分两步组成:

1.重定位节和符号定义。这一步中,连接器将所有相同类型的节合并起来,并为它们赋予存储器地址。当这一步完成时,程序中的每个指令和全局变量都有唯一的运行时存储器地址了。
2.重定位节中的符号引用。这一步,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的地址。

通过链接,就会生成可执行目标文件hello

再看以下hello反汇编的main部分代码:

080483e4 <main>:
 80483e4:	55                   	push   %ebp
 80483e5:	89 e5                	mov    %esp,%ebp
 80483e7:	83 e4 f0             	and    $0xfffffff0,%esp
 80483ea:	83 ec 10             	sub    $0x10,%esp
 80483ed:	c7 04 24 c0 84 04 08 	movl   $0x80484c0,(%esp)
 80483f4:	e8 1f ff ff ff       	call   8048318 <puts@plt>
 80483f9:	c9                   	leave
 80483fa:	c3                   	ret
 80483fb:	90                   	nop
 80483fc:	90                   	nop
 80483fd:	90                   	nop
 80483fe:	90                   	nop
 80483ff:	90                   	nop

看到之前需要重定位的两个地方现在都有了确切的地址了。

运行

然后就可以通过./hello运行啦

在Linux中,每个程序都有一个运行时存储器映像,如下图所示(图中“从可执行文件中加载那个大括号括错了,应该往下平移下):

用readelf -l hello可以看到只读段和读写段分别占用哪段地址:

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
LOAD           0x000000 0x08048000 0x08048000 0x004d4 0x004d4 R E 0x1000
LOAD           0x000f0c 0x08049f0c 0x08049f0c 0x00108 0x00110 RW  0x1000

用readelf -S hello可以看到所有节的的信息:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048134 000134 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048148 000148 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE            08048168 000168 000024 00   A  0   0  4
  [ 4] .hash             HASH            0804818c 00018c 000028 04   A  6   0  4
  [ 5] .gnu.hash         GNU_HASH        080481b4 0001b4 000020 04   A  6   0  4
  [ 6] .dynsym           DYNSYM          080481d4 0001d4 000050 10   A  7   1  4
  [ 7] .dynstr           STRTAB          08048224 000224 00004a 00   A  0   0  1
  [ 8] .gnu.version      VERSYM          0804826e 00026e 00000a 02   A  6   0  2
  [ 9] .gnu.version_r    VERNEED         08048278 000278 000020 00   A  7   1  4
  [10] .rel.dyn          REL             08048298 000298 000008 08   A  6   0  4
  [11] .rel.plt          REL             080482a0 0002a0 000018 08   A  6  13  4
  [12] .init             PROGBITS        080482b8 0002b8 000030 00  AX  0   0  4
  [13] .plt              PROGBITS        080482e8 0002e8 000040 04  AX  0   0  4
  [14] .text             PROGBITS        08048330 000330 00016c 00  AX  0   0 16
  [15] .fini             PROGBITS        0804849c 00049c 00001c 00  AX  0   0  4
  [16] .rodata           PROGBITS        080484b8 0004b8 000016 00   A  0   0  4
  [17] .eh_frame         PROGBITS        080484d0 0004d0 000004 00   A  0   0  4
  [18] .ctors            PROGBITS        08049f0c 000f0c 000008 00  WA  0   0  4
  [19] .dtors            PROGBITS        08049f14 000f14 000008 00  WA  0   0  4
  [20] .jcr              PROGBITS        08049f1c 000f1c 000004 00  WA  0   0  4
  [21] .dynamic          DYNAMIC         08049f20 000f20 0000d0 08  WA  7   0  4
  [22] .got              PROGBITS        08049ff0 000ff0 000004 04  WA  0   0  4
  [23] .got.plt          PROGBITS        08049ff4 000ff4 000018 04  WA  0   0  4
  [24] .data             PROGBITS        0804a00c 00100c 000008 00  WA  0   0  4
  [25] .bss              NOBITS          0804a014 001014 000008 00  WA  0   0  4
  [26] .comment          PROGBITS        00000000 001014 000048 01  MS  0   0  1
  [27] .shstrtab         STRTAB          00000000 00105c 0000ee 00      0   0  1
  [28] .symtab           SYMTAB          00000000 0015fc 000410 10     29  45  4
  [29] .strtab           STRTAB          00000000 001a0c 0001fb 00      0   0  1

在shell中敲入./hello后,shell会先调用fork()函数创建一个新的哦进程,然后调用exec()函数来加载hello程序,加载器会删除子进程现有的虚拟存储器段,并创建一组新的代码、数据、堆和栈段。其中堆和栈被初始化为零,代码段和数据段被初始化为hello中的相应内容。最后加载器跳转到_start,而它最终会调用hello中的main函数。

(注:加载器并不会把代码段和数据段直接拷贝到内存中,而是在页表中将相应条目指向文件的适当位置,在每个页被引用时,会引发缺页中断,然后虚拟存储器系统会按照需要自动的跳入相应页)

——————————————————————————————————-

以下面这段代码为例,看下C语言中各变量的存储位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
int g_not_init1;
int g_not_init2;
int g_init1 = 1;
int g_init2 = 2;
int main()
{
     char *str = "abc" ;
     int local1 = 1;
     int local2 = 2;
     static int static1 = 1;
     static int static2 = 2;
     int *p = ( int *) malloc ( sizeof ( int ));
     printf ( "main:%p\n" , main);
     printf ( "g_not_init1:%p\n" , &g_not_init1);
     printf ( "g_not_init2:%p\n" , &g_not_init2);
     printf ( "g_init1:%p\n" , &g_init1);
     printf ( "g_init2:%p\n" , &g_init2);
     printf ( "local1:%p\n" , &local1);
     printf ( "local2:%p\n" , &local2);
     printf ( "static1:%p\n" , &static1);
     printf ( "static2:%p\n" , &static2);
     printf ( "p:%p\n" , p);
     printf ( "str:%p\n" , str);
}

运行结果:

main:0x8048414
g_not_init1:0x804a034
g_not_init2:0x804a030
g_init1:0x804a018
g_init2:0x804a01c
local1:0xbfeb3a78
local2:0xbfeb3a74
static1:0x804a020
static2:0x804a024
p:0x8191008
str:0x80485f0

用readelf -S hello查看下节信息:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048134 000134 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048148 000148 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE            08048168 000168 000024 00   A  0   0  4
  [ 4] .hash             HASH            0804818c 00018c 00002c 04   A  6   0  4
  [ 5] .gnu.hash         GNU_HASH        080481b8 0001b8 000020 04   A  6   0  4
  [ 6] .dynsym           DYNSYM          080481d8 0001d8 000060 10   A  7   1  4
  [ 7] .dynstr           STRTAB          08048238 000238 000053 00   A  0   0  1
  [ 8] .gnu.version      VERSYM          0804828c 00028c 00000c 02   A  6   0  2
  [ 9] .gnu.version_r    VERNEED         08048298 000298 000020 00   A  7   1  4
  [10] .rel.dyn          REL             080482b8 0002b8 000008 08   A  6   0  4
  [11] .rel.plt          REL             080482c0 0002c0 000020 08   A  6  13  4
  [12] .init             PROGBITS        080482e0 0002e0 000030 00  AX  0   0  4
  [13] .plt              PROGBITS        08048310 000310 000050 04  AX  0   0  4
  [14] .text             PROGBITS        08048360 000360 00026c 00  AX  0   0 16
  [15] .fini             PROGBITS        080485cc 0005cc 00001c 00  AX  0   0  4
  [16] .rodata           PROGBITS        080485e8 0005e8 000089 00   A  0   0  4
  [17] .eh_frame         PROGBITS        08048674 000674 000004 00   A  0   0  4
  [18] .ctors            PROGBITS        08049f0c 000f0c 000008 00  WA  0   0  4
  [19] .dtors            PROGBITS        08049f14 000f14 000008 00  WA  0   0  4
  [20] .jcr              PROGBITS        08049f1c 000f1c 000004 00  WA  0   0  4
  [21] .dynamic          DYNAMIC         08049f20 000f20 0000d0 08  WA  7   0  4
  [22] .got              PROGBITS        08049ff0 000ff0 000004 04  WA  0   0  4
  [23] .got.plt          PROGBITS        08049ff4 000ff4 00001c 04  WA  0   0  4
  [24] .data             PROGBITS        0804a010 001010 000018 00  WA  0   0  4
  [25] .bss              NOBITS          0804a028 001028 000010 00  WA  0   0  4
  [26] .comment          PROGBITS        00000000 001028 000048 01  MS  0   0  1
  [27] .shstrtab         STRTAB          00000000 001070 0000ee 00      0   0  1
  [28] .symtab           SYMTAB          00000000 001610 000480 10     29  47  4
  [29] .strtab           STRTAB          00000000 001a90 000251 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

用readelf -l hello查看下段信息,注意这里还打印出了段与节的对应关系(Section to Segment mapping):

Elf file type is EXEC (Executable file)
Entry point 0x8048360
There are 8 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
  INTERP         0x000134 0x08048134 0x08048134 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00678 0x00678 R E 0x1000
  LOAD           0x000f0c 0x08049f0c 0x08049f0c 0x0011c 0x0012c RW  0x1000
  DYNAMIC        0x000f20 0x08049f20 0x08049f20 0x000d0 0x000d0 RW  0x4
  NOTE           0x000148 0x08048148 0x08048148 0x00044 0x00044 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
  GNU_RELRO      0x000f0c 0x08049f0c 0x08049f0c 0x000f4 0x000f4 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .note.gnu.build-id .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag .note.gnu.build-id
   06
   07     .ctors .dtors .jcr .dynamic .got

结合前面程序运行结果,很容易可以发现如下事实:

main函数的地址0?8048414属于.text节,放在只读段中

常量字符串“abc”的地址(也就是str指针指向的地址)0x80485f0属于.rodata节,也放在只读段中

已初始化的两个全局变量(地址分别为0x804a018和0x804a01c)属于.data节,放在读写段中

未初始化的两个全局变量属于.bss节,放在读写段中

两个初始化的静态变量与初始化过的全局变量一样,属于.data节

可以猜到(代码中没写),未初始化的静态变量应该和未初始化的全局变量类似了

两个局部变量的地址0xbfeb3a78和0xbfeb3a74,看下上面程序运行时的存储器映像图就明白了,局部变量是放在栈中的,而且是从高地址向低地址增长,这里都得到了验证。

最后剩下p,它由malloc分配,所以应该放在运行时的堆中,可以看到它的位置在读写段的后面

好了,以后再也不怕类似的笔试题了= =。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值