ELF文件格式、编译过程和符号表

ELF文件介绍

image.png
ELF的英文全称是The Executable and Linking Format,最初是由UNIX系统实验室开发、发布的ABI(Application Binary Interface)接口的一部分,也是Linux的主要可执行文件格式。
比如说用户空间的.out就是一个ELF的文件
一个程序的3个基本段:text段,data段,bss段。
text段:就是放程序代码的,编译时确定,只读;
data段:存放在编译阶段(而非运行时)就能确定的数据,可读可写。也就是通常所说的静态存储区,赋了初值的全局变量赋初值的静态变量存放在这个区域,常量也存放在这个区域;
bss段:定义而没有赋初值的全局变量和静态变量,放在这个区域;比如我定义了一个全局变量int a;但是我没有给它赋值。那么就是在这个段。
dec和hex是总的大小,不同的进制。
如果你定义了一个局部变量,不会占用ELF文件的空间(也就是你size一下是看不到这个占的空间在哪),因为它是程序运行的时候储存在栈里面的,编译的时候是没有空间的。

用户空间的ELF文件

// main.c
#include <stdio.h>

int global_var = 10;

void foo() {
    printf("Hello, World!\n");
}

int main() {
    foo();
    printf("Global variable: %d\n", global_var);
    return 0;
}

编译完成用size查看大小

jubuntu@ubuntu:~$ size test100
   text	   data	    bss	    dec	    hex	filename
   1765	    612	      4	   2381	    94d	test100

这里为什么bss为4我也不知道?GPT给出的答案是
如果通过size命令显示**.bss**段的大小为4,可能有以下几个原因:

  1. 编译器优化:编译器可能对代码进行了优化,将对全局变量的初始化转化为对**.bss段的预留空间。这种优化可以减少可执行文件的大小。编译器可能会决定将已初始化的全局变量存储在.bss段,而不是.data**段。
  2. 特定编译器和编译选项:不同的编译器和编译选项可能会对全局变量的存储方式有所不同。某些编译器或编译选项可能会将已初始化的全局变量存储在**.bss**段。

太细节我就不深究了,目前还无法把握。那我们换一个程序看一下:

可以看到不同数据占的空间

hjubuntu@ubuntu:~/driver_linux/param$ size modulea.ko
   text	   data	    bss	    dec	    hex	filename
    614	    900	      0	   1514	    5ea	modulea.ko

如果ELF文件格式里面没有给局部变量留空间的话,那么程序运行的时候,CPU怎么能够识别到局部变量?我的意思是既然ELF文件里面没有局部变量,那么局部变量是怎么提取到栈里面去的呢?局部变量先前是作为代码指令储存在.text段里面的吗?

文件编译过程

image.png

  1. 预处理(Preprocessing):预处理器根据指令(以**‘#’**开头)对源文件进行处理,包括宏替换、头文件展开、条件编译等。预处理的结果是生成一个经过宏替换和条件编译处理的纯C代码文件。(当然注释在这一步肯定被删掉了)
  2. 编译(Compilation):编译器将预处理后的代码进行词法分析、语法分析和语义分析,生成中间表示(如抽象语法树或中间代码)。然后进行优化处理,包括控制流优化、数据流优化等。最后,将优化后的代码翻译成汇编代码。
  3. 汇编(Assembly):汇编器将汇编代码转换成机器指令,并生成目标文件(通常是以**.o**为扩展名)。目标文件包含了机器指令和相关的符号信息,但尚未解析符号引用。
  4. 链接(Linking):链接器将目标文件与其他目标文件、库文件进行链接,解析符号引用,并生成最终的可执行文件或共享库。在链接过程中,链接器会根据符号引用和定义进行符号解析,建立符号之间的关联关系。

比如写一个简单的程序

#include <stdio.h>

#define PI 3.14

int main() {
    int i=666;
    printf("hello world PI=%f\n",PI);
    printf("hello world i=%d\n",i);
    return 0;
}

带分号的是语句,预处理指令(#开头)和注释都不会被编译和编程机器码,在编译之前把这些处理掉。
我们把这个预处理一下 gcc -E test1.c -o test1.i
查看.i预处理文件

/*前面省略几百行代码*/
# 5 "test1.c"
int main() {
    int i=666;
    printf("hello world PI=%f\n",3.14);
    printf("hello world i=%d\n",i);
    return 0;
}

可以看出头文件被展开(就是省略的几百行代码,这里没有展示),宏定义被替换。(这里面有一个局部变量,int i=666;这个声明语句最终会转化为机器指令储存在.text中,这些带;的语句最终都会转化为机器指令储存在.text中,当函数执行到局部变量的声明语句时,栈指针会被调整以分配适当的空间给局部变量 i。此时,i 的初始值 666(这个值本身) 会被存储在栈上的分配空间中。)
现在我们把它.i文件生成.s文件(汇编文件)
gcc -S test1.i -o test1.s
查看文件,一些汇编代码(下面展示的是部分)

main:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	$666, -4(%rbp)
	movq	.LC0(%rip), %rax
	movq	%rax, %xmm0
	leaq	.LC1(%rip), %rdi
	movl	$1, %eax
	call	printf@PLT
	movl	-4(%rbp), %eax
	movl	%eax, %esi
	leaq	.LC2(%rip), %rdi
	movl	$0, %eax
	call	printf@PLT
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

可以看到,局部变量的声明也被转化为了汇编语言
最后再把test1.s生成test1.o
gcc -c test1.s -o test1.o
gedit查看二进制不方便,这里不展示
接下来是链接,我们看一下链接是咋个回事
链接静态库、动态库,多文件编译
多文件编译
比如你在v.c里面定义了一个全局变量i

int i =100;
#include <stdio.h>

#define PI 3.14
extern int i;

int main() {

    printf("before declaring the local variation i=%d\n",i);
	
    int i=666;
    printf("hello world PI=%f\n",PI);
    printf("after declaring the local variation i=%d\n",i);
    
    return 0;
}

然后运行如下shell命令,先把他们生成.o文件再链接

hjubuntu@ubuntu:~/test$ gcc -c v.c -o v.o
hjubuntu@ubuntu:~/test$ gcc -c test1.c -o test1.o
hjubuntu@ubuntu:~/test$ gcc test1.0 v.o -o test
gcc: error: test1.0: 没有那个文件或目录
hjubuntu@ubuntu:~/test$ gcc test1.o v.o -o test
hjubuntu@ubuntu:~/test$ ./test
before declaring the local variation i=100
hello world PI=3.140000
after declaring the local variation i=666
hjubuntu@ubuntu:~/test$ 

这里面有个小知识点,就是当全局变量和局部变量同时存在的时候(比如i有全局定义和局部定义),优先选用局部变量。
链接静态库/动态库:
看下面第三节

  • 库是一个二进制文件,包含的代码可被程序调用
  • 标准c库、数学库、线程库…
  • 库有源码,可下载后编译;也可以直接安装二进制包
  • 默认在 /lib目录和 /usr/lib
  • 库是事先编译好的,可以复用的代码。
  • 在os上运行的程序基本上都要使用库。使用库可以提高开发效率。
  • Windows和Linux下库文件的格式不兼容
  • Linux下包含静态库和共享库
  • 可执行文件包含main函数(必须有)

静态库

image.png
为什么浪费空间,你想啊,假如你不同的可执行文件都需要一个同样的库,那么你在每一个可执行文件里面都需要粘贴上这个库,当然占空间
image.png

总的流程如下:

  1. 编写库文件代码hello.c

image.png
这个程序是不能直接编译运行的,因为没有main函数。只能生成一个.o文件,是不能执行的。现在需要创建成库文件。

  1. 编译hello.c生成hello.o

image.png
库文件以lib开头,.a为后缀,由.o文件生成,.o文件由.c文件生成
image.png

  1. 由hello.o生成库文件

image.png
image.png
image.png

  1. 写test.c

image.png
image.png
image.png

  1. 把库文件链接到test文件上

image.png
image.png

11.2 动态库(共享库)

image.png

image.png
image.png

这上面的.1我也不知道是啥,视频里面敲的时候也没有加.1
image.png
image.png

这上面这所以报错是因为不会在当前目录找这个文件(而是在/usr/lib或者/lib),有以下几个方法

image.png

推荐使用环境变量

总的流程如下:

动态库的后缀是.so

  1. 写一个bye.c

image.png

  1. 还有之前的hello.c

image.png

  1. 编译两个.c文件生成目标文件

image.png

  1. 创建共享库libmyheby.so

image.png

  1. 编写测试(应用)程序test.c

image.png

  1. 为共享库创建链接文件,链接到test文件上

image.png

  1. 把当前目录添加到环境目录

image.png

也可以用绝对路径<br />![image.png](https://img-blog.csdnimg.cn/img_convert/e42db3fa2450b0ce0798cd7f202cca6f.png#averageHue=#302e28&clientId=u9a3c3688-de81-4&from=paste&height=57&id=u8eec3117&originHeight=78&originWidth=1050&originalType=binary&ratio=2&rotation=0&showTitle=false&size=69493&status=done&style=none&taskId=ua6548f97-8d32-40c7-89bb-16bb5901126&title=&width=764)

如果你在新的窗口打开又不行了

需要重新设置环境变量,因此可以在bashrc文件末尾加上,之后再source一下(每次修改.bashrc后,使用source就可以立刻加载修改后的设置,使之生效。)

export LD_LIBRARY_PATH=/mnt/hgfs/share/newIOP/day5

每次启动自动设置环境变量。这样子的话可以环境变量在任何一个窗口都有效了
image.png

.bashrc是home目录下的一个shell文件,用于储存用户的个性化设置。在bash每次启动时都会加载

  1. 查看文件所使用的动态库

image.png

标记的就是上面讲的库文件,其他的可能是一些标准库文件啥的吧。

2.1 目标文件

目标文件是编译器生成的中间文件,它包含了编译后的代码和相关的元数据。目标文件的具体格式取决于所使用的目标文件格式(如ELF、COFF等),不同格式的目标文件有不同的结构和特点。
对于ELF目标文件(常用于Linux系统),它通常由多个节(section)组成,每个节负责存储不同类型的信息。一些常见的节包括:

  1. .text:代码段,存储编译后的机器指令。
  2. .data:数据段,存储初始化的全局变量和静态变量。
  3. .bss:未初始化的数据段,存储未初始化的全局变量和静态变量。
  4. .rodata:只读数据段,存储常量字符串和其他只读数据。
  5. .symtab:符号表节,存储符号的名称、类型、绑定信息和地址等。
  6. .rel.text:重定位表节,存储需要进行代码段重定位的位置和信息。

以上只是一些常见的节,目标文件的具体结构可能更加复杂。
对于未解析符号引用,它指的是在目标文件中遇到的符号引用,但没有找到对应的定义。这些引用通常是由于目标文件引用了其他模块或库中定义的符号,但在当前目标文件中无法解析。这可能是因为目标文件还没有与其他模块进行链接,链接器尚未处理符号解析。
在链接过程中,链接器会尝试解析这些未定义的符号引用。它会搜索其他目标文件、库文件和系统提供的符号表,以找到对应的符号定义。一旦找到匹配的符号定义,链接器会将未解析的符号引用与定义进行关联,并进行正确的地址重定位。
(这一块可以看一下模块参数的文档,一个模块中定义了变量,导出到符号表之后,可以给别的模块使用)
总之,目标文件是编译器生成的中间文件,它包含了编译后的代码和相关的元数据。未解析符号引用是指目标文件中遇到的符号引用,但在当前目标文件中找不到对应的定义。链接过程会尝试解析这些未定义的符号引用,并与定义进行匹配和关联。

符号表

开头图片里面的symtab就是符号表(里面有些函数名,汇编标识符啥的)
之前用size是查看能够在CPU上运行的数据(可以加载到内存给CPU运行的),可以理解成就是data,bss,text这些吧,但是实际上一个ELF程序是不止这些的。可以用ll filename查看。
nm可以列出文件的符号表

hjubuntu@ubuntu:~/driver_linux/param$ nm modulea.ko
0000000000000000 T cleanup_module
000000008ccc5fc0 A __crc_gx
                 U __fentry__
0000000000000000 D gx
0000000000000000 T init_module
0000000000000000 r __kstrtab_gx
0000000000000003 r __kstrtabns_gx
0000000000000000 r __ksymtab_gx
0000000000000000 T modulea_exit
0000000000000000 T modulea_init
0000000000000018 r _note_8
0000000000000000 r _note_9
                 U _printk
0000000000000000 D __this_module
000000000000002f r __UNIQUE_ID_depends120
0000000000000000 r __UNIQUE_ID_license118
0000000000000044 r __UNIQUE_ID_name118
0000000000000038 r __UNIQUE_ID_retpoline119
000000000000000c r __UNIQUE_ID_srcversion121
0000000000000051 r __UNIQUE_ID_vermagic117
0000000000000000 r ____versions
                 U __x86_return_thunk


通过strip命令可以把符号表删除掉,给文件瘦身。
CPU在执行程序的时候,只有text,data,bss会被加载到内存,但是编译的时候符号表又会被保存到ELF文件的格式里面,所以会导致ELF文件比较大,所以符号表删掉对文件本身的运行时没有什么影响的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值