我们考虑从shell中执行一个Linux应用程序,并且该应用程序链接的是动态库,而不是静态库

1. 加载二进制文件

shell会执行evecve()进行系统调用,如下所示 execve() -> do_sys_execve() -> do_execve() –> do_execve_common()

这里先介绍内核的两个数据结构

~ struct linux_binfmt:  用来支持多种二进制格式(a.out, elf)

~ struct linux_binprm: 保存要要执行的文件相关的信息

do_execve_common(const char *filename, struct user_arg_ptr argv, struct user_arg_ptr envp)

首先进行权限检查,然后将linux_binprm结构体初始化,接着调用search_binary_handle()

值得注意的事do_execve_common()在prepare_binprm()中将filename(要执行的二进制文件)的前128个字节读入至linux_binprm->buf中

search_binary_handle()遍历formats链表(所有注册的二进制格式),依次尝试二进制格式的load_binary()

对于elf格式,为load_elf_binary(struct linux_binprm *bprm)

这里重点讨论load_elf_binary()

首先检查会检查是否为ELF格式,然后根据ELF header(loc->elf_ex,内容即linux_binprm->buf)将Program header(简称为Ph)读到内存中,

然后遍历Ph找到类型为PT_INTERP的Segment,读取该Segment的内容(即解释器文件路径,一般为/lib/ld-linux.so.2)

打开解释器,将头128个字节读入到linux_binprm->buf中,这样我们就得到了解释器的ELF Header信息(存至局部变量loc->interp_elf_ex中)

再次遍历Ph找到类型为PT_GNU_STACK的Segment,查看是否具有可执行标记,接着设置进程内存setup_arg_pages()

再再次遍历Ph找到类型为PT_LOAD的Segment,将它们映射到进程的内存区域中(elf_map()),设置text/data/bss/stack等值

执行load_elf_interp()加载解释器至内存,并把返回的地址(即动态链接器入口地址)设置为elf_entry(程序入口地址)

TIP:

当execve退出的时候动态链接器接着运行

动态连接器检查应用程序对共享连接库的依赖性,并在需要时对其进行加载,对程序的外部引用进行重定位。

然后动态连接器把控制权交给应用程序,从ELF文件头部中定义的程序进入点开始执行

然后调用create_elf_tables(),它将argc、argv等,还有一些辅助向量(Auxiliary Vector)等信息复制到用户空间

最后调用start_thread() 修改了pt_regs中寄存器信息

其中cp0_epc=elf_entry, 使其指向加载的应用程序的入口(一般均有解释器,故elf_entry实际值为解释器入口地址)

TIP: 以上过程均在内核空间完成

2. ELF的链接过程

该过程也可称之为解释器的执行过程。解释器或者动态加载器,负责加载动态库

对于一个程序来说,若需要链接动态库,需要在编译时指定解释器的位置

gcc会根据specs文件来将解释器的位置写入到到程序中

下面是specs文件的寻找顺序

~1 -specs=

~2 系统的specs配置文件,位置是 [`dirname $(gcc --print-libgcc-file-name)`/specs]

~3 gcc在编译时build-in的spces,可通过gcc -dumpspecs查看

我们gcc -dumpspecs可以找到解释器的位置(由dynamic_linker指定)

这也是Linux上解释器的一般位置

*dynamic_linker:

/lib/ld-linux.so.2

解释器实际为ld-version.so(后用ld.so表示),

它是由glibc提供,主要由glibc-version/elf/目录下rtld.c, dl-xxx.c等文件编译而成

注意,解释器是静态编译的并且不具备共享库依赖项。

ld.so入口函数为RTLD_START, (_dl_start为C实际入口点)

(已有elf文件找到入口点方法: 查看elf header找到entry,objdump -S反汇编,找到entry处的函数)

前面说到,从内核空间返回后,入口地址是解释器的入口地址,即接下来轮到解释器当主角了。

ld.so实际完成的工作包括查找和加载动态共享库

@1 查找共享库ld.so查找共享库的顺序如下(Really?? check manual)

1. 在可执行的目标文件中被指定,即DT_RPATH所指定路径;在编译目标代码时,链接参数"-Wl,-rpath"来指定

2. 缺省在/usr/lib和lib中搜索

3. LD_LIBRARY_PATH环境变量中所设定的路径

4. /etc/ld.so.conf中所指定的路径

TIP: 将lib放到/usr/lib,/lib或者修改了/etc/ld.so.config需要执行ldconfig来更新ld.so.cache来生效

@2 加载共享库

首先来介绍两个概念

~~ GOT(global offset table)

在位置无关代码中,使用的都是相对地址,当在程序中引用某个共享库中的符号时,编译链接阶段并不知道这个符号的具体位置,只有等到动态链接器将所需要的共享库加载时进内存后,也就是在运行阶段,符号的地址才会最终确定。因此,需要有一个数据结构来保存符号的绝对地址,这就是GOT表的作用,GOT表中每项保存程序中引用其它符号的绝对地址。这样,程序就可以通过引用GOT表来获得某个符号的地址。

在x86结构中,GOT表的前三项保留,用于保存特殊的数据结构地址,其它的各项保存符号的绝对地址。

对于符号的动态解析过程,我们只需要了解的就是第二项和第三项,即GOT[1]和GOT[2]

GOT[1]    保存的是一个地址,指向已经加载的共享库的链表地址(struct link_map)

GOT[2]    保存的是_dl_runtime_resolve()的地址

~~ PLT(procedure linkage table)

过程链接表(PLT)的作用就是将位置无关的函数调用转移到绝对地址。在编译链接时,外部函数的地址还不能确定,因此,链接器将控制转移到PLT中的某一项。而PLT通过引用GOT表中的函数的绝对地址,来把控制转移到实际的函数。

[FIXME]对于加了-fPIC选项的共享库

.got.plt: 存放外部函数的地址

.got: 存放全局变量的地址

.rel.got

.plt

.rel.plt

.rel.dyn

在实际的可执行程序或者共享目标文件中,GOT表在名称为.got.plt的section中,PLT表在名称为.plt的section中

[root@bogon ~]# cat printf.c

int main()

{

printf("Hello World!\n");

return 0;

}

[root@bogon ~]# gcc printf.c -o printf

[root@bogon ~]# objdump -S printf

...

Disassembly of section .plt:

0804828c <__gmon_start__>:

804828c: ff 35 08 96 04 08 pushl 0x8049608

8048292: ff 25 0c 96 04 08 jmp *0x804960c /* 跳到*GOT[2],即_dl_runtime_resolve */

8048298: 00 00 add %al,(%eax)

...

0804829c <__gmon_start__>:

804829c: ff 25 10 96 04 08 jmp *0x8049610

80482a2: 68 00 00 00 00 push $0x0

80482a7: e9 e0 ff ff ff jmp 804828c <_init>

080482ac <__libc_start_main>:

80482ac: ff 25 14 96 04 08 jmp *0x8049614

80482b2: 68 08 00 00 00 push $0x8

80482b7: e9 d0 ff ff ff jmp 804828c <_init>

080482bc :

80482bc: ff 25 18 96 04 08 jmp *0x8049618

80482c2: 68 10 00 00 00 push $0x10

80482c7: e9 c0 ff ff ff jmp 804828c <_init>

Disassembly of section .text:

080482d0 <_start>:

...

080483a4 :

80483a4: 8d 4c 24 04 lea 0x4(%esp),%ecx

80483a8: 83 e4 f0 and $0xfffffff0,%esp

80483ab: ff 71 fc pushl 0xfffffffc(%ecx)

80483ae: 55 push %ebp

80483af: 89 e5 mov %esp,%ebp

80483b1: 51 push %ecx

80483b2: 83 ec 04 sub $0x4,%esp

80483b5: c7 04 24 a0 84 04 08 movl $0x80484a0,(%esp)

80483bc: e8 fb fe ff ff call 80482bc

80483c1: b8 00 00 00 00 mov $0x0,%eax

80483c6: 83 c4 04 add $0x4,%esp

80483c9: 59 pop %ecx

80483ca: 5d pop %ebp

80483cb: 8d 61 fc lea 0xfffffffc(%ecx),%esp

80483ce: c3 ret

80483cf: 90 nop

...

.plt Section可以看成一个数组,每个元素为16字节。

该Section依次为PLT[0](即__gmon_start__@plt-0x10), PLT[1], PLT[2], PLT[3](puts@plt)

该Section遵循如下格式(N>=0)

PLT[0]: push &GOT[1]

jmp GOT[2] @points to resolver(), _dl_runtime_resolve()

PLT[n+1]: jmp *GOT[n+3]

push #n @push n as a signal to the resolver

jmp PLT[0]

下面分析如何得到puts的实际地址

当main函数执行到put时候,跳转到*0x8049618(即GOT[5])处,当前这里的内容其实是它的下一条指令push $0x10的,也就是会顺序执行,到第三条指令,即PLT[0]处再接着往下执行跳转到_dl_runtime_resolve,该解析到puts的地址,并保存puts的地址到GOT[5]中,

这样后面如果再执行到该处,就可以直接跳转到puts来执行。

3. 二进制文件的执行

ELF文件的实际的入口函数式是_start的地址。

控制传递给_start以后,_start从由内核设置的栈中获取参数和环境变量信息,然后调用__libc_start_main。

__libc_start_main初始化必要的数据结构,尤其是C库(比如malloc)和线程环境,然后调用用户的main函数。值得注意的是,__libc_start_main认为main

函数是: int main(int argc,  char ** argv, char ** env)。

main函数的返回值由__libc_start_main接收,并传递给exit。

linux如何调试elf程序,Linux下ELF的执行过程相关推荐

  1. linux如何调试elf程序,Linux应用程序elf描述

    玩Linux的人应该明白ELF文件是一种文件格式,就好比.txt,.doc等一样,只是这个文件是按照特定信息排列组成,同样在windows上也存在一种格式,它叫PE,老的叫dos.下面我就来看看ELF ...

  2. delve应该安装到哪_使用 Delve 代替 Println 来调试 Go 程序 | Linux 中国

    Delve 是能让调试变成轻而易举的事的万能工具包.来源:https://linux.cn/article-12400-1.html 作者:Gaurav Kamathe 译者:Xiaobin.Liu ...

  3. linux如何调试脚本程序,调试Linux shell脚本的方法

    在linux中调试shell脚本,常用的有三个方法.这里介绍下,希望对大家有所帮助. 方法一,使用echo命令. 在调试shell脚本时,可以用echo打印任何变量值,以判断错误原因. 方法二,she ...

  4. linux如何运行java程序,Linux环境下运行简单java程序

    一.安装java 1.下载jdk8 选择对应jdk版本下载.(Tips:可在Windows下载完成后,通过FTP或者SSH到发送到Linux上) 2. 登录Linux,切换到root用户 su roo ...

  5. linux中调试脚本,在Linux下调试 Shell 脚本

    在大多数编程语言中都有调试工具可用于调试. 调试工具可以运行需要调试的程序或脚本,使我们可以在运行时检查脚本或程序的内部执行过程. 在shell脚本中我们没有任何调试工具,只能借助命令行选项(-n,- ...

  6. qt单步调试linux程序,用Qt 调用GDB调试 Arm程序 详细步骤----可单步执行每一行

    前言 本人交叉编译环境 Ubuntu 10.04(虚拟机),编译工具链 arm-hisiv100nptl-linux,Qt 4.8.5 ,QtCreator1.3.1 1.在虚拟机Ubuntu 10. ...

  7. linux c代码调试工具,在 Linux 中调试 C 程序的福音——gdb

    如果你是 C/C++ 程序员,或者使用 Fortran 和 Modula-2 编程语言开发软件,那么你将会很乐意知道有这么一款优秀的调试器 - GDB - 可以帮你更轻松地调试代码 bug 以及其它问 ...

  8. linux中python安装_linux环境下的python安装过程图解(含setuptools)

    这里我不想采用诸如ubuntu下的apt-get install方式进行python的安装,而是在linux下采用源码包的方式进行python的安装. 一.下载python源码包 打开ubuntu下的 ...

  9. linux 中断 c语言程序,linux驱动之中断处理过程C程序部分

    当发生中断之后,linux系统在汇编阶段经过一系列跳转,最终跳转到asm_do_IRQ()函数,开始C程序阶段的处理.在汇编阶段,程序已经计算出发生中断的中断号irq,这个关键参数最终传递给asm_d ...

最新文章

  1. java 多路分发_java实现多路分发
  2. 如何成为CSDN博客专家
  3. SAP UI5 new sap.ui.commons.Button trigger component load
  4. oracle把多行合并成字符串,怎样将Oracle多行转换成字符串?
  5. 【定时器/中断/PWM】利用一个定时器实现一路PWM波的输出---点亮LED
  6. Intel 64/x86_64/IA-32/x86处理器 - SIMD指令集 - SSE扩展(3) - MXCSR寄存器详解
  7. 美国一鹦鹉趁主人不在家上网购物:买的都是水果蔬菜
  8. postgre sql 括字段_【技术干货】30个最适合初学者的SQL查询
  9. C++ 11使用thread类多线程编程
  10. android nfc MifareUltralight读写
  11. android手机给iphone越狱,在越狱的iPhone上安装Android 2.2教程
  12. Java项目:校园二手交易平台(java+SSM+Thymeleaf+Html+jQuery+mysql)
  13. C++ 信息管理系统
  14. 音质好的linux主机,实测:ASIO 的音质更好?
  15. 阿里云服务器使用宝塔面板管理以及项目部署
  16. 【牛客】CPU的运算速度与许多因素有关,下面______是提高速度的有效措施?
  17. 大学计算机专业分为哪几类
  18. {JSONDecodeError}Expecting value: line 1 column 1 (char 0)
  19. vue中的观察者模式
  20. 文末送书!看懂这本书,程序员可以自信地说“我要打十个”!

热门文章

  1. COMSOL软件入门仿真框架建立及软件基本操作
  2. 查看classpath
  3. Banned Patterns 计蒜客 - A1533
  4. 解决无法获取 GridView 中BoundField 隐藏列值问题
  5. 美国《福布斯》热评:“中国是老大”
  6. mysql的依赖_MySQL 依赖关系/部分依赖关系/间接依赖关系
  7. 常使用投射,折射等各类新型光源和灯具
  8. JavaScript~~~入门~~~
  9. 超难智力题(答案几日后公布)
  10. ASP.NET实例:用C#制作艺术字