含大量图文解析及例程 | Linux下的ELF文件、链接、加载与库(上)
sinye56 2024-11-27 20:31 1 浏览 0 评论
常用工具
我们首先列出一些在接下来的介绍过程中会频繁使用的分析工具,如果从事操作系统相关的较底层的工作,那这些工具应该再熟悉不过了。不熟悉的读者可以先看一下这里的简单的功能介绍,我们会在后文中介绍一些详细的参数选项和使用场景。
另外,建议大家在遇到自己不熟悉的命令时,通过 man 命令来查看手册,这是最权威的、第一手的资料。
ELF文件详解
ELF文件的三种形式
在Linux下,可执行文件/动态库文件/目标文件(可重定向文件)都是同一种文件格式,我们把它称之为ELF文件格式。虽然它们三个都是ELF文件格式但都各有不同。以下文件的格式信息可以通过 file 命令来查看。
- 可重定位(relocatable)目标文件:通常是.o文件。包含二进制代码和数据,其形式可以再编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行(executable)目标文件:是完全链接的可执行文件,即静态链接的可执行文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。
- 共享(shared)目标文件:通常是.so动态链接库文件或者动态链接生成的可执行文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。注意动态库文件和动态链接生成的可执行文件都属于这一类。会在最后一节辨析时详细区分。
因为我们知道ELF的全称:Executable and Linkable Format,即 ”可执行、可链接格式“,很显然这里的三个ELF文件形式要么是可执行的、要么是可链接的。
其实还有一种core文件,也属于ELF文件,在core dumped时可以得到。我们这里暂且不提。
注意:在Linux中并不以后缀名作为区分文件格式的绝对标准。
节头部表和程序头表和ELF头
在我们的ELF文件中,有两张重要的表:节头部表(Section Tables)和程序头表(Program Headers)。可以通过readelf -l [fileName]和readelf -S [fileName]来查看。
但并不是所有以上三种ELF的形式都有这两张表,
- 如果用于编译和链接(可重定位目标文件),则编译器和链接器将把elf文件看作是节头表描述的节的集合,程序头表可选。
- 如果用于加载执行(可执行目标文件),则加载器则将把elf文件看作是程序头表描述的段的集合,一个段可能包含多个节,节头部表可选。
- 如果是共享目标文件,则两者都含有。因为链接器在链接的时候需要节头部表来查看目标文件各个 section 的信息然后对各个目标文件进行链接;而加载器在加载可执行程序的时候需要程序头表 ,它需要根据这个表把相应的段加载到进程自己的的虚拟内存(虚拟地址空间)中。
我们在后面的还会详细介绍这两张表。
此外,整个ELF文件的前64个字节,成为ELF头,可以通过readelf -h [fileName]来查看。我们也会在后面详细介绍。
可重定位ELF文件的内容分析
#include <elf.h>,该头文件通常在/usr/include/elf.h,可以自己vim查看。
首先有一个64字节的ELF头Elf64_Ehdr,其中包含了很多重要的信息(可通过readelf -h [fileName]来查看),这些信息中有一个很关键的信息叫做Start of section headers,它指明了节头部表,Section Headers Elf64_Shdr的位置。段表中储存了ELF文件中各个的偏移量以记录其位置。ELF中的各个段可以通过readelf -S [fileName]来查看。
其中各个节的含义如下:
这样我们就把一个可重定位的ELF文件中的每一个字节都搞清楚了。
静态链接
编译、链接的需求
嵌入式物联网需要学的东西真的非常多,千万不要学错了路线和内容,导致工资要不上去!
无偿分享大家一个资料包,差不多150多G。里面学习内容、面经、项目都比较新也比较全!某鱼上买估计至少要好几十。
点击这里找小助理0元领取:https://s.pdb2.com/l/cnklSITCGo24eIn
为了节省空间和时间,不将所有的代码都写在同一个文件中是一个很基本的需求。
为此,我们的C语言需要实现这样的需求:允许引用其他文件(C标准成为编译单元,Compilation Unit)里定义的符号。C语言中不禁止你随便声明符号的类型,但是类型不匹配是Undefined Behavior。
假如我们有三个c文件,分别是a.c,b.c,main.c:
// a.c
int foo(int a, int b){
return a + b;
}
// b.c
int x = 100, y = 200;
// main.c
extern int x, y;
int foo(int a, int b);
int main(){
printf("%d + %d = %d\n", x, y, foo(x, y));
}
我们在main.c中声明了外部变量x,y和函数foo,C语言并不禁止我们这么做,并且在声明时,C也不会做什么类型检查。当然,在编译main.c的时候,我们看不到这些外部变量和函数的定义,也不知道它们在哪里。
我们编译链接这些代码,Makfile如下:
CFLAGS := -Os
a.out: a.o b.o main.o
gcc -static -Wl,--verbose a.o b.o main.o
a.o: a.c
gcc $(CFLAGS) -c a.c
b.o: b.c
gcc $(CFLAGS) -c b.c
main.o: main.c
gcc $(CFLAGS) -c main.c
clean:
rm -f *.o a.out
结果生成的可执行文件可以正常地输出我们想要的内容。
make
./a.out
# 输出:
# 100 + 200 = 300
我们知道foo这个符号是一个函数名,在代码区。但这时,如果我们将main.c中的foo声明为一个整型,并且直接打印出这个整型,然后尝试对其加一。即我们将main.c改写为下面这样,会发生什么事呢?
// main.c (changed)
#include <stdio.h>
extern int x, y;
// int foo(int a, int b);
extern int foo;
int main(){
printf("%x\n", foo);
foo += 1;
// printf("%d + %d = %d\n", x, y, foo(x, y));
}
输出:
c337048d
Segmentation fault (core dumped)
我们发现,其实是能够打印出四个字节(整型为4个字节),但这四个字节是什么东西呢?
C语言中的类型:C语言中的其实是可以理解为没有类型的,在C语言的眼中只有内存和指针,也就是内存地址,而所谓的C语言中的类型,其实就是对这个地址的一个解读。比如有符号整型,就按照补码解读接下来的4个字节地址;又比如浮点型,就是按照IEEE754的浮点数规定来解读接下来的4字节地址。
那我们这里将符号foo定义为了整型,那编译器也会按照整型4个自己来解读它,而这个地址指针指向的其实还是函数foo的地址。那这四个字节应该就是函数foo在代码段的前四个字节。我们不妨用objdump反汇编来验证我们的想法:
objdump -d a.out
输出(节选):
我们看到,foo函数在代码段的前四个字节的地址确是就是我们上面打印输出的c3 37 04 8d(注意字节序为小端法)。
那我们接下来试图对foo进行加一操作相当于是对代码段的写操作,而我们知道内存中的代码段是 可读可执行不可写 的,这就对应了上面输出的Segmentation fault (core dumped)。
总结一下,通过这个例子,我们应当理解:
- 编译链接的需求:允许引用其他文件(C标准成为编译单元,Compilation Unit)里定义的符号。C语言中不禁止你随便声明符号的类型,但是类型不匹配是Undefined Behavior。
- C语言中类型的概念:C语言中的其实是可以理解为没有类型的,在C语言的眼中只有内存和指针,也就是内存地址,而所谓的C语言中的类型,其实就是对这个地址的一个解读。
程序的编译 - 可重定向文件
我们先用file命令来查看main.c编译生成的main.o文件的属性:
file main.o
输出:
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
我们看到这里的main.o文件是可重定向( relocatable) 的ELF文件,这里的重定向指的就是我们链接过程中对外部符号的引用。也就是说,编译过的main.o文件对于其中声明的外部符号如foo,x,y,是不知道的。
既然外部的符号是在链接时才会被main程序知道,那在编译main程序,生成可重定向文件时这些外部的符号是怎么处理的呢?我们同样通过objdump工具来查看编译出的main.o文件(未修改的原版本):
objdump -d main.o
输出:
main在编译的时候,引用的外部符号就只能 ”留空(0)“ 了。
我们看到,在编译但还未链接的main.o文件中,对于引用的外界符号的部分是用留空的方式用0暂时填充的。即上图中红框框出来的位置。注意图中的最后一列是笔者添加的注释,指明了本行中留空的地方对应那个外部符号。
另外注意这里的%rip相对寻址的偏移量都是0,一会儿我们会讲到,在静态链接完成之后,它们的偏移量会被填上正确的数值。
我们已经知道在编译时生成的文件中外部符号的部分使用0暂时留空的,这些外部符号是待链接时再填充的。那么,我们在链接时究竟需要填充哪些位置呢?我们可以使用readelf工具来查看ELF文件的重定位信息:
readelf -r main.o
这个图中上方是readelf的结果,下面是objdump的结果,笔者在这里已经将前两个外部符号的偏移量的对应关系用红色箭头指了出来,其他的以此类推。这种对应也可以证明我们上面的分析是正确的的。
应当讲,可重定向ELF文件(如main.o)已经告诉了我们足够多的信息,指示我们应该将相应的外部符号填充到哪个位置。
另外,注意%rip寄存器指向了当前指令的末尾,也就是下一条指令的开头,所以上图中最后的偏移量要减4(如 y - 4)。
程序的静态链接
简单讲,程序的静态链接是会把所需要的文件链接起来生成可执行的二进制文件,将相应的外部符号,填入正确的位置(就像我们上面查看的那样)。
- 段的合并
首先会做一个段的合并。即把相同的段(比如代码段 .text)识别出来并放在一起。
- 重定位
重定位表,可用objdump -r [fileName] 查看。
简单讲,就是当某个文件中引用了外部符号,在编译时编译器是不会阻止你这样做的,因为它相信你会在链接时告诉它这些外部符号是什么东西。但在编译时,它也不知到这些符号具体在什么地址,因此这些符号的地址会在编译时被留空为0。此时的重定位,就是链接器将这些留空为0的外部符号填上正确的地址。
具体的链接过程,可以通过ld --verbose来查看默认的链接脚本,并在需要的时候修改链接脚本。
我们可以通过使用gcc的 -Wl,--verbose将--verbose传递给链接器ld,从而直接观察到整个静态链接的过程,包括:
- ldscript里面各个section是按照何种顺序 “粘贴”
- ctors / dtors (constructors / destructores) 的实现,( 我们用过__attribute__((contructor)) )
- 只读数据和读写数据之间的padding,. = DATA_SEGMENT_ALIGN …
我们可以通过objdump来查看静态链接完成以后生成的可执行文件a.out的内容:
objdump -d a.out
注意,这个a.out的objdump结果图要与我们之前看到的main.o的objdump输出对比着来看。
我们可以看到,之前填0留空的地方都被填充上了正确的数值,%rip相对寻址的偏移量以被填上了正确的数值,而且objdump也能够正确地解析出我们的外部符号名(最后一列)的框。
静态链接库的构建与使用
假如我们要制作一个关于向量的静态链接库libvector.a,它包含两个源代码addvec.c和multvec.c如下:
// addvec.c
int addcnt = 0;
void addvec(int *x, int *y, int*z, int n){
int i;
addcnt++;
for (i=0; i<n; i++) z[i] = x[i] + y[i];
}
// multvec.v
int multcnt = 0;
void multvec(int *x, int *y, int*z, int n){
int i;
multcnt++;
for (i=0; i<n; i++) z[i] = x[i] * y[i];
}
我们只需要这样来进行编译:
gcc -c addvec.c multvec.c
ar rcs libvector.a addvec.o multvec.o
假如我们有个程序main.c要调用这个静态库libvector.a:
// main.c
#include <stdio.h>
#include "vector.h"
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main(){
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
return 0;
}
// vector.h
void addvec(int*, int*, int*, int);
void multvec(int*, int*, int*, int);
只需要在这样编译链接即可:
gcc -c main.c
gcc -static main.o ./libvector.a
静态链接过程图示
我们以使用刚才构建的静态库libvector.a的程序为例,画出静态链接的过程。
原文作者:人人极客
原文链接:https://mp.weixin.qq.com/s/9EYNSyi10mYHhlwuP7z4aw
原文标题:含大量图文解析及例程 | Linux下的ELF文件、链接、加载与库(上)
相关推荐
- Linux基础知识之修改root用户密码
-
现象:Linux修改密码出现:Authenticationtokenmanipulationerror。故障解决办法:进入单用户,执行pwconv,再执行passwdroot。...
- Linux如何修改远程访问端口
-
对于Linux服务器而言,其默认的远程访问端口为22。但是,出于安全方面的考虑,一般都会修改该端口。下面我来简答介绍一下如何修改Linux服务器默认的远程访问端口。对于默认端口而言,其相关的配置位于/...
- 如何批量更改文件的权限
-
如果你发觉一个目录结构下的大量文件权限(读、写、可执行)很乱时,可以执行以下两个命令批量修正:批量修改文件夹的权限chmod755-Rdir_name批量修改文件的权限finddir_nam...
- CentOS「linux」学习笔记10:修改文件和目录权限
-
?linux基础操作:主要介绍了修改文件和目录的权限及chown和chgrp高级用法6.chmod修改权限1:字母方式[修改文件或目录的权限]u代表所属者,g代表所属组,o代表其他组的用户,a代表所有...
- Linux下更改串口的权限
-
问题描述我在Ubuntu中使用ArduinoIDE,并且遇到串口问题。它过去一直有效,但由于可能不必要的原因,我觉得有必要将一些文件的所有权从root所有权更改为我的用户所有权。...
- Linux chown命令:修改文件和目录的所有者和所属组
-
chown命令,可以认为是"changeowner"的缩写,主要用于修改文件(或目录)的所有者,除此之外,这个命令也可以修改文件(或目录)的所属组。当只需要修改所有者时,可使用...
- chmod修改文件夹及子目录权限的方法
-
chmod修改文件夹及子目录权限的方法打开终端进入你需要修改的目录然后执行下面这条命令chmod777*-R全部子目录及文件权限改为777查看linux文件的权限:ls-l文件名称查看li...
- Android 修改隐藏设置项权限
-
在Android系统中,修改某些隐藏设置项或权限通常涉及到系统级别的操作,尤其是针对非标准的、未在常规用户界面显示的高级选项。这些隐藏设置往往与隐私保护、安全相关的特殊功能有关,或者涉及开发者选项、权...
- 完蛋了!我不小心把Linux所有的文件权限修改了!在线等修复!
-
最近一个客户在群里说他一不小心把某台业务服务器的根目录权限给改了,本来想修改当前目录,结果执行成了根目录。...
- linux改变安全性设置-改变所属关系
-
CentOS7.3学习笔记总结(五十八)-改变安全性设置-改变所属关系在以前的文章里,我介绍过linux文件权限,感兴趣的朋友可以关注我,阅读一下这篇文章。这里我们不在做过的介绍,注重介绍改变文件或者...
- Python基础到实战一飞冲天(一)--linux基础(七)修改权限chmod
-
#07_Python基础到实战一飞冲天(一)--linux基础(七)--修改权限chmod-root-groupadd-groupdel-chgrp-username-passwd...
- linux更改用户权限为root权限方法大全
-
背景在使用linux系统时,经常会遇到需要修改用户权限为root权限。通过修改用户所属群组groupid为root,此操作只能使普通用户实现享有部分root权限,普通用户仍不能像root用户一样享有超...
- 怎么用ip命令在linux中添加路由表项?
-
在Linux中添加路由表项,可以使用ip命令的route子命令。添加路由表项的基本语法如下:sudoiprouteadd<network>via<gateway>这...
- Linux配置网络
-
1、网卡名配置相关文件回到顶部网卡名命名规则文件:/etc/udev/rules.d/70-persistent-net.rules#PCIdevice0x8086:0x100f(e1000)...
- Linux系列---网络配置文件
-
1.网卡配置文件在/etc/sysconfig/network-scripts/下:[root@oldboynetwork-scripts]#ls/etc/sysconfig/network-s...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle忘记用户名密码 (59)
- oracle11gr2安装教程 (55)
- mybatis调用oracle存储过程 (67)
- oracle spool的用法 (57)
- oracle asm 磁盘管理 (67)
- 前端 设计模式 (64)
- 前端面试vue (56)
- linux格式化 (55)
- linux图形界面 (62)
- linux文件压缩 (75)
- Linux设置权限 (53)
- linux服务器配置 (62)
- mysql安装linux (71)
- linux启动命令 (59)
- 查看linux磁盘 (72)
- linux用户组 (74)
- linux多线程 (70)
- linux设备驱动 (53)
- linux自启动 (59)
- linux网络命令 (55)
- linux传文件 (60)
- linux打包文件 (58)
- linux查看数据库 (61)
- linux获取ip (64)
- linux进程通信 (63)