linux如何调试elf程序,Linux下ELF的执行过程
我们考虑从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的执行过程相关推荐
- linux如何调试elf程序,Linux应用程序elf描述
玩Linux的人应该明白ELF文件是一种文件格式,就好比.txt,.doc等一样,只是这个文件是按照特定信息排列组成,同样在windows上也存在一种格式,它叫PE,老的叫dos.下面我就来看看ELF ...
- delve应该安装到哪_使用 Delve 代替 Println 来调试 Go 程序 | Linux 中国
Delve 是能让调试变成轻而易举的事的万能工具包.来源:https://linux.cn/article-12400-1.html 作者:Gaurav Kamathe 译者:Xiaobin.Liu ...
- linux如何调试脚本程序,调试Linux shell脚本的方法
在linux中调试shell脚本,常用的有三个方法.这里介绍下,希望对大家有所帮助. 方法一,使用echo命令. 在调试shell脚本时,可以用echo打印任何变量值,以判断错误原因. 方法二,she ...
- linux如何运行java程序,Linux环境下运行简单java程序
一.安装java 1.下载jdk8 选择对应jdk版本下载.(Tips:可在Windows下载完成后,通过FTP或者SSH到发送到Linux上) 2. 登录Linux,切换到root用户 su roo ...
- linux中调试脚本,在Linux下调试 Shell 脚本
在大多数编程语言中都有调试工具可用于调试. 调试工具可以运行需要调试的程序或脚本,使我们可以在运行时检查脚本或程序的内部执行过程. 在shell脚本中我们没有任何调试工具,只能借助命令行选项(-n,- ...
- qt单步调试linux程序,用Qt 调用GDB调试 Arm程序 详细步骤----可单步执行每一行
前言 本人交叉编译环境 Ubuntu 10.04(虚拟机),编译工具链 arm-hisiv100nptl-linux,Qt 4.8.5 ,QtCreator1.3.1 1.在虚拟机Ubuntu 10. ...
- linux c代码调试工具,在 Linux 中调试 C 程序的福音——gdb
如果你是 C/C++ 程序员,或者使用 Fortran 和 Modula-2 编程语言开发软件,那么你将会很乐意知道有这么一款优秀的调试器 - GDB - 可以帮你更轻松地调试代码 bug 以及其它问 ...
- linux中python安装_linux环境下的python安装过程图解(含setuptools)
这里我不想采用诸如ubuntu下的apt-get install方式进行python的安装,而是在linux下采用源码包的方式进行python的安装. 一.下载python源码包 打开ubuntu下的 ...
- linux 中断 c语言程序,linux驱动之中断处理过程C程序部分
当发生中断之后,linux系统在汇编阶段经过一系列跳转,最终跳转到asm_do_IRQ()函数,开始C程序阶段的处理.在汇编阶段,程序已经计算出发生中断的中断号irq,这个关键参数最终传递给asm_d ...
最新文章
- java 多路分发_java实现多路分发
- 如何成为CSDN博客专家
- SAP UI5 new sap.ui.commons.Button trigger component load
- oracle把多行合并成字符串,怎样将Oracle多行转换成字符串?
- 【定时器/中断/PWM】利用一个定时器实现一路PWM波的输出---点亮LED
- Intel 64/x86_64/IA-32/x86处理器 - SIMD指令集 - SSE扩展(3) - MXCSR寄存器详解
- 美国一鹦鹉趁主人不在家上网购物:买的都是水果蔬菜
- postgre sql 括字段_【技术干货】30个最适合初学者的SQL查询
- C++ 11使用thread类多线程编程
- android nfc MifareUltralight读写
- android手机给iphone越狱,在越狱的iPhone上安装Android 2.2教程
- Java项目:校园二手交易平台(java+SSM+Thymeleaf+Html+jQuery+mysql)
- C++ 信息管理系统
- 音质好的linux主机,实测:ASIO 的音质更好?
- 阿里云服务器使用宝塔面板管理以及项目部署
- 【牛客】CPU的运算速度与许多因素有关,下面______是提高速度的有效措施?
- 大学计算机专业分为哪几类
- {JSONDecodeError}Expecting value: line 1 column 1 (char 0)
- vue中的观察者模式
- 文末送书!看懂这本书,程序员可以自信地说“我要打十个”!