从代码到程序加载执行

一、代码编译过程图

在这里插入图片描述

二、代码编译及编译文件分析

比如有如下的测试代码:
process1.c

#include <stdio.h>
void local_func1(){
        printf("local_func1\n");
}
void static local_func2(){
        printf("local_func2\n");
}
void func1(){
        printf("func1\n");
        local_func1();
        local_func2();
}

process2.c

#include <stdio.h>
void func2(){
        printf("func2\n");
}

对process1.c和process2.c进行编译:

gcc -fPIC -o process1.o -c process1.c
gcc -fPIC -o process2.o -c process2.c

process*.o文件头结构类似于可执行文件结构(见上一篇)
其中:
1、由于只是编译,未进行链接,文件中的符号位置没固定,所以程序入口位置(Entry point address)为0

[work@bogon 10]$ readelf -h process1.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:          1080 (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: 12

下面看一下文件中section部分:
其中:
.text部分为代码部分
.rela.text为重定位节部分,包含重定位偏移位置,重定位类型(这样在链接的时候,根据该重定位表信息,重定位.text部分,同理.rela.data)
.data为全局初始化变量
.bss为全局未初始化变量,默认初始化为0
.rodata为只读数据,包括字符串常量,const变量,如下:
.rodata
.comment是版本控制信息如下:
.comment
.symtab是符号表信息,为本程序导出的符号信息,如下:
.symtab
.strtab符号的字符串信息,一般为符号表的字符串名称
.strtab.shstrtab section字符串信息,一般为所有的section字符串名称
.shstrtab

[work@bogon 10]$ readelf -S process1.o
There are 13 section headers, starting at offset 0x438:
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000004a  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  000002e0
       00000000000000a8  0000000000000018   I      10     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000008a
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000008a
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  0000008a
       000000000000001e  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000a8
       000000000000002e  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000d6
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000d8
       0000000000000078  0000000000000000   A       0     0     8	
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000388
       0000000000000048  0000000000000018   I      10     8     8
  [10] .symtab           SYMTAB           0000000000000000  00000150
       0000000000000150  0000000000000018          11    10     8
  [11] .strtab           STRTAB           0000000000000000  000002a0
       000000000000003f  0000000000000000           0     0     1
  [12] .shstrtab         STRTAB           0000000000000000  000003d0
       0000000000000061  0000000000000000           0     0     1

三1.静态库.a生成及生成可执行程序

静态库里若只压一个.o,其实和.o没有区别,例如:

ar cr libprocess1.a process1.o

在这里插入图片描述
更多情况下,我们是把多个.o文件压到一个静态库中的,如下:

ar cr libprocess.a process1.o process2.o

我们看一下libprocess.a中内容:
在这里插入图片描述
发现静态库其实就是直接把.o组合在一起的

下面编译一下main.c文件
其中Type字段为NOTYPE信息的,需要在链接的时候从其他的文件中链接过来

gcc -fPIC -o main.o -c main.c

readelf -s main.o
Symbol table '.symtab' contains 19 entries:
  Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_var_init
    10: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_val_uninit
    11: 0000000000000000   104 FUNC    GLOBAL DEFAULT    1 main
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND malloc
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND memcpy
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    16: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND free
    17: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND func1
    18: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND func2

此时main.o也是可定位的文件,里面的虚拟地址也未指定,如下:
(e8为函数跳转指令)

在这里插入图片描述
下面进行静态库链接:

gcc -o main main.o -L . -lprocess

链接过程是根据可重定位文件中的可重定位表进行重定位,此时对符号进行了定位,分配了虚拟地址:
在这里插入图片描述

三2.动态库.so生成及生成可执行程序

gcc -shared -fPIC -o libprocess.so process1.o process2.o

因为生成.so的时候是使用到连接器的,所有分配了虚拟地址,若要使用.so生成可执行文件,需要对.so中符号进行重定位,所有需要使用-fPIC命令。
动态库中的结构基本上和.o文件类似。
下面看一下动态库中的动态符号表:
动态符号表中导出的函数是可以被外部文件调用的。
在这里插入图片描述
某些时候,我们不希望把我们所有的函数 或者 存在不希望外面文件调用的函数,或者内部存在函数名称和外部文件中函数名称一致,为了避免版本冲突,可以不把该函数加到符号表内,这样外部文件链接的时候就不会找到该函数了,使用如下命令:

gcc -shared -o libprocess2.so process1.o process2.o -fPIC -Wl,--version-script=./process.map
[work@bogon 10]$ cat process.map 
{
global:
        func1;
        func2;
local:*;
};

在这里插入图片描述链接:

gcc -o main main.o ./libprocess2.so

四,可执行程序加载准备

1、对于单个进程来说,可操作的是虚拟内存地址,所有的虚拟内存空间在该进程来看都是独享的
2、对于32位机器来说,寻址范围为4G,内存管理把整个寻址空间分为3G给用户进程空间,1G给内核进程空间
首先基于动态库创建的可执行程序,有些不同,多了一个.interp段:

[work@bogon 10]$ readelf -S main
There are 30 section headers, starting at offset 0x1a90:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1

查看一下这个段里面的内容:

[work@bogon 10]$ readelf -x 1 main

Hex dump of section '.interp':
  0x00400238 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
  0x00400248 7838362d 36342e73 6f2e3200          x86-64.so.2.

发现这个里面是个动态链接器,也就是程序运行时的链接是它做的。

/下面时引用自极客时间刘超老师课程/
另外,ELF 文件中还多了两个 section,一个是.plt,过程链接表(Procedure Linkage Table,PLT),一个是.got.plt,全局偏移量表(Global Offset Table,GOT)。
main这个程序要调用 libprocess.so 里的函数。由于是运行时才去找,编译的时候,不知道这个函数在哪里,所以就在 PLT 里面建立一项 PLT[x]。这一项也是一些代码,有点像一个本地的代理,在二进制程序里面,不直接调用 func1 函数,而是调用 PLT[x]里面的代理代码,这个代理代码会在运行的时候找真正的func1函数。去哪里找代理代码呢?这就用到了 GOT,这里面也会为func1 函数创建一项 GOT[y]。这一项是运行时 func1 函数在内存中真正的地址。如果这个地址在 main调用 PLT[x]里面的代理代码,代理代码调用 GOT 表中对应项 GOT[y],调用的就是加载到内存中的 libprocess.so 里面的 func1 函数了。但是 GOT 怎么知道的呢?对于 func1 函数,GOT 一开始就会创建一项 GOT[y],但是这里面没有真正的地址,因为它也不知道,但是它有办法,它又回调 PLT,告诉它,你里面的代理代码来找我要 func1函数的真实地址,我不知道,你想想办法吧。PLT 这个时候会转而调用 PLT[0],也即第一项,PLT[0]转而调用 GOT[2],这里面是 ld-linux.so 的入口函数,这个函数会找到加载到内存中的 libcprocess.so 里面的func1 函数的地址,然后把这个地址放在 GOT[y]里面。下次,PLT[x]的代理函数就能够直接调用了。

我理解的是,程序加载的时候,会加载这个链接器程序,当程序运行时,调用链接器来查找函数,PLT(里面有函数的名字),链接器根据GOT(这里面有动态库.so的地址),然后加载对应的.so,.so加载后,获取指定func的真实虚拟地址返回给代码。

五、可执行程序加载过程和运行内存图

shell中执行可执行程序加载过程(进程fork再执行,类似):
1、shell判断可执行文件类型。(shell脚本?可执行程序?)
2、可执行程序的话,fork子进程,执行execve系统调用
3、execve系统调用执行do_execve函数
4、do_execve->do_execveat_common->__do_execve_file
5、创建linux_binprm->根据本用户命名空间创建内存管理结构bprm_mm_init
6、根据inode读取二进制文件的前128字节到linux_binprm结构体的file字段(kernel_read)
7、search_binary_handler
8、search_binary_handle->load_binary
9、load_elf_binary加载二进制的elf程序,把可执行程序需要加载的段,加载到指定的虚拟内存地址中。
10、exec_binprm执行
11、
下面是我测试程序main运行时,内存空间分布:
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值