1. 什么是库
库是写好的现成的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都是从零开始,因此库的存在意义就非同寻常。
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:
- 静态库 .a [Linux]、.lib [windows]
- 动态库 .so[Linux]、.dll [windows]
// ubuntu 动静态库
// C
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rwxr-xr-x 1 root root 2029592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so
$ ls -l /lib/x86_64-linux-gnu/libc.a
-rw-r--r-- 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.a
//C++
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l
lrwxrwxrwx 1 root root 40 Oct 24 2022 /usr/lib/gcc/x86_64-linuxgnu/9/libstdc++.so -> ../../../x86_64-linux-gnu/libstdc++.so.6
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
/usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
// Centos 动静态库
// C
$ ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so
[whb@bite-alicloud ~]$ ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5105516 Jun 4 23:05 /lib64/libc.a
// C++
$ ls /lib64/libstdc++.so.6 -l
lrwxrwxrwx 1 root root 19 Sep 18 20:59 /lib64/libstdc++.so.6 ->
libstdc++.so.6.0.19
$ ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l
-rw-r--r-- 1 root root 2932366 Sep 30 2020 /usr/lib/gcc/x86_64-redhatlinux/4.8.2/libstdc++.a
下面我们, 先自主封装一个libc代码,创建一个“库文件”
// my_stdio.h
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
int flag; // 刷新⽅式
int fileno; // ⽂件描述符
char outbuffer[SIZE];
int cap;
int size;
// TODO
};
typedef struct IO_FILE mFILE;
mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
// my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE *mfopen(const char *filename, const char *mode)
{
int fd = -1;
if(strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if(strcmp(mode, "w")== 0)
{
fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
}
else if(strcmp(mode, "a") == 0)
{
fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);
}
if(fd < 0) return NULL;
mFILE *mf = (mFILE*)malloc(sizeof(mFILE));
if(!mf)
{
close(fd);
return NULL;
}
mf->fileno = fd;
mf->flag = FLUSH_LINE;
mf->size = 0;
mf->cap = SIZE;
return mf;
}
void mfflush(mFILE *stream)
{
if(stream->size > 0)
{
// 写到内核⽂件的⽂件缓冲区中!
write(stream->fileno, stream->outbuffer, stream->size);
// 刷新到外设
fsync(stream->fileno);
stream->size = 0;
}
}
int mfwrite(const void *ptr, int num, mFILE *stream)
{
// 1. 拷⻉
memcpy(stream->outbuffer+stream->size, ptr, num);
stream->size += num;
// 2. 检测是否要刷新
if(stream->flag == FLUSH_LINE && stream->size > 0 && stream-
>outbuffer[stream->size-1]== '\n')
{
mfflush(stream);
}
return num;
}
void mfclose(mFILE *stream)
{
if(stream->size > 0)
{
mfflush(stream);
}
close(stream->fileno);
}
// my_string.h
#pragma once
int my_strlen(const char *s);
// my_string.c
#include "my_string.h"
int my_strlen(const char *s)
{
const char *end = s;
while(*end != '\0')end++;
return end - s;
}
2. 静态库
- 静态库(.a): 程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库、
- 一个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译器默认为动态链接库,只有在该库找不到动态. so 的时候才会采用同名静态。我们也可以使用 gcc 的 -static 强制设置链接静态库。
2.1 静态库生成
// Makefile
libmystdio.a:my_stdio.o my_string.o
@ar -rc $@ $^
@echo "build $^ to $@ ... done"
%.o:%.c
@gcc -c $<
@echo "compling $< to $@ ... done"
.PHONY:clean
clean:
@rm -rf *.a *.o stdc*
@echo "clean ... done"
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.a stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done
注:
ar是GNU的归档工具,用于创建、修改和提取归档文件(通常是将多个文件和目录打包成一个文件),用ar工具创建的归档文件通常具有 .a 或 .lib 扩展名。
上面代码的 含义就是将my_stdio.o 和 my_string.o 文件打包成一个名为 libstdio.a的静态库,其中r 表示将文件插入归档文件(如果归档文件不存在则创建),c表示创建归档文件,s表示创建索引
提取归档文件内容 ar x libstdio.a 可以将libstdio.a 中的文件提取出来。
$ ar -tv libmystdio.a
rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o
rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o
这里 t:列出静态库中的文件 v:verbose 详细信息
2.2 静态库的使用
#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>
int main()
{
const char *s = "abcdefg";
printf("%s: %d\n", s, my_strlen(s));
mFILE *fp = mfopen("./log.txt", "a");
if(fp == NULL) return 1;
mfwrite(s, my_strlen(s), fp);
mfwrite(s, my_strlen(s), fp);
mfwrite(s, my_strlen(s), fp);
mfclose(fp);
return 0;
}
注:
- 场景1:头⽂件和库⽂件安装到系统路径下
- $ gcc main.c -lmystdio
- 场景2:头⽂件和库⽂件和我们⾃⼰的源⽂件在同⼀个路径下
- $ gcc main.c -L. -lmymath
- 场景3:头⽂件和库⽂件有⾃⼰的独⽴路径
- $ gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
-L : 指定库路径 -I :指定头文件搜索路径 -l:指定库名
测试目标文件生成后,静态库删掉,程序照样可以运行
库文件名称和引入库的名称: 去掉前缀 lib, 去掉后缀 .so ,.a,如 libc.so/.a -> c
3. 动态库
动态库(.so):程序在运行的时候才会去链接动态库的代码,多个程序共享使用库的代码。
一个动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
3.1 动态库生成
// Makefile
libmystdio.so:my_stdio.o my_string.o
gcc -o $@ $^ -shared
%.o:%.c
gcc -fPIC -c $<
.PHONY:clean
clean:
@rm -rf *.so *.o stdc*
@echo "clean ... done"
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.so stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
-shared:表示生成共享库格式(就是告诉编译器不要形成可执行程序,帮我形成.so库)
-fPIC:产生位置无关码(position independent code) -c -> .o
库名规则:libxxx.so
3.2 动态库的使用
使用上和上面静态库的链接方式相同,这里说一下下面这个指令
ldd libmystdio.so // 查看库或者可执⾏程序的依赖
linux-vdso.so.1 => (0x00007fffacbbf000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)
ldd 可以查看库或者可执行程序的依赖
3.3 库运行搜索路径
3.3.1 问题
当我们移动动态链接的动态库后,或者当我们可执行程序,库和头文件分开存储时,
如下:
gcc test.c -L./stdc/lib -I./stdc/include -lmystdio
我们在查看它的依赖关系就会变成如下
ldd a.out
linux-vdso.so.1 => (0x00007fff4d396000)
libmystdio.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007fa2aef30000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)
这时,我们再次运行这个程序,发现运行不了了,那么这是为什么呢,明明我们都已经指定了库和头文件的位置了,为什么会找不到呢?
其实我们在编译时指明的库和头文件位置,其实只是告诉了编译器,库和头文件的位置,编译结束生成可执行文件,运行可执行文件的是OS,而OS还不知道库和头文件的位置,所以才会造成这样的问题。
那么下面我们来说下这种问题该如何解决
3.3.2 解决方案(可执行文件OS搜索路径)
- 拷贝.so文件到系统共享库路径下,一般指 /usr/lib 、 /usr/local/lib、/lib64 或者开篇指明的库路径等。
- 向系统共享库路径下建立同名软链接(快捷方式)
- 更改环境变量: LD_LIBRARY_PATH(export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:自己库的路径)
- Idconfig方案:自定义一个.conf 文件写好自己库的路径添加到/etc/ld.so.conf.d, 然后输入ldconfig更新到内核链接缓存(这里可以使用ldconfig -p 来查看当前系统的所有链接缓存)
注:部分系统可执行文件还会搜索文件坐在目录下的库
4. 使用外部库
我们现在没接触过太多的库,唯一接触过的就是C、C++标准库,这里我们可以推荐一个好玩的图形库:ncurses
// 安装
// Centos
$ sudo yum install -y ncurses-devel
// ubuntu
$ sudo apt install -y libncurses-dev
系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况的函数(ncurses库)
// 我们代码是我⽤⽂⼼⼀⾔⽣成并调试通过的,具体要什么功能,⼤家可以试着了解⼀下
#include <stdio.h>
#include <string.h>
#include <ncurses.h>
#include <unistd.h>
#define PROGRESS_BAR_WIDTH 30
#define BORDER_PADDING 2
#define WINDOW_WIDTH (PROGRESS_BAR_WIDTH + 2 * BORDER_PADDING + 2) // 加边框的宽
度
#define WINDOW_HEIGHT 5
#define PROGRESS_INCREMENT 3
#define DELAY 300000 // 微秒(
300毫秒)
int main() {
initscr();
start_color();
init_pair(1, COLOR_GREEN, COLOR_BLACK); // 已完成部分:绿⾊前景,⿊⾊背景
init_pair(2, COLOR_RED, COLOR_BLACK); // 剩余部分(虽然⽤红⾊可能不太合适,但为演⽰⽬的):红⾊背景
cbreak();
noecho();
curs_set(FALSE);
int max_y, max_x;
getmaxyx(stdscr, max_y, max_x);
int start_y = (max_y - WINDOW_HEIGHT) / 2;
int start_x = (max_x - WINDOW_WIDTH) / 2;
WINDOW *win = newwin(WINDOW_HEIGHT, WINDOW_WIDTH, start_y, start_x);
box(win, 0, 0); // 加边框
wrefresh(win);
int progress = 0;
int max_progress = PROGRESS_BAR_WIDTH;
while (progress <= max_progress) {
werase(win); // 清除窗⼝内容
// 计算已完成的进度和剩余的进度
int completed = progress;
int remaining = max_progress - progress;
// 显⽰进度条
int bar_x = BORDER_PADDING + 1; // 进度条在窗⼝中的x坐标
int bar_y = 1; // 进度条在窗⼝中的y坐标(居中)
// 已完成部分
attron(COLOR_PAIR(1));
for (int i = 0; i < completed; i++) {
mvwprintw(win, bar_y, bar_x + i, "#");
}
attroff(COLOR_PAIR(1));
// 剩余部分(⽤背景⾊填充)
attron(A_BOLD | COLOR_PAIR(2)); // 加粗并设置背景⾊为红⾊(仅⽤于演⽰)
for (int i = completed; i < max_progress; i++) {
mvwprintw(win, bar_y, bar_x + i, " ");
}
attroff(A_BOLD | COLOR_PAIR(2));
// 显⽰百分⽐
char percent_str[10];
snprintf(percent_str, sizeof(percent_str), "%d%%", (progress * 100) /
max_progress);
int percent_x = (WINDOW_WIDTH - strlen(percent_str)) / 2; // 居中显⽰
mvwprintw(win, WINDOW_HEIGHT - 1, percent_x, percent_str);
wrefresh(win); // 刷新窗⼝以显⽰更新
// 增加进度
progress += PROGRESS_INCREMENT;
// 延迟⼀段时间
usleep(DELAY);
}
// 清理并退出ncurses模式
delwin(win);
endwin();
return 0;
}
推荐一篇不错的使⽤指南:https://blog.csdn.net/bdn_nbd/article/details/134019142
下面我们在用一张图来深入理解下动态库的加载与进程地址空间的关系
首先,当程序开始运行并需要加载动态库时,OS首先会进行动态库的定位。不同的系统会有不同的搜索路径,Linux下,会查看LD_LIBRARY_PATH环境变量指定的路径以及一些默认的系统路径。
接着,加载过程,OS会将硬盘中的动态库映射到进程的虚拟地址空间,而这一过程通常是通过系统调用实现的,如Linux中的mmap函数。它不会立刻把整个动态库文件内容都加载到物理内存,而是采用了一种 “按需加载“的策略,即只有当程序访问动态库中的某个部分(如函数或变量)时,对应的部分才会被真正加载到物理内存,这种方式会节省内存空间
最后是符号解析。动态库加载后,程序需要调用其中的函数或访问变量。这时就涉及到符号解析,也就是把程序中引用的符号(函数名、变量名等)和动态库中实际的定义位置对应起来操作系统维护了一个符号表,用于记录这些映射关系。当程序第一次调用动态库中的函数时,会通过符号表查找对应的地址,之后就可以直接跳转执行。
上图就是对于mmap过程的模拟
OS会现将动态库的相关部分(可能是全部,也可能是部分取决于具体实现和参数)映射到内存的共享区,在访问动态库中被映射区域的数据时,会产生一个缺页中断(后面会说)。这是因为一开始数据可能没有真正加载到物理内存(按需加载),当发生缺页中断时,内核会根据映射关系,从磁盘上的动态库文件中将对应的页面(内存管理的基本单位)数据读取到物理内存,并更新页表,使虚拟地址能够正确访问到物理内存中数据。
进程通过页表将共享库映射到自己的共享区,这也使得我们进程在执行库方法时,是在自己的地址空间中跳转运行的!
5. 目标文件
编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们一般都是一键构建非常方便,但一旦遇到错误的时候呢,尤其是链接相关的错误,很多人就束手无策了。在Linux下,我们之前也学习过如何通过gcc编译器来完成这一系列操作。
接下来我们深入探讨一下编译和链接的整个过程,来更好的理解动静态库的使用原理。
先来回顾下什么是编译呢? 编译的过程其实就是我们将程序源代码翻译成CPU能够直接运行的机器代码。
比如:在一个源文件hello.c 里便简单输出 "hello world!",并且调用一个run函数,而这个函数被定义在另一个源文件code.c 中。这里我们就可以调用 gcc -c 来分别编译这两个源文件。
// hello.c
#include<stdio.h>
void run();
int main() {
printf("hello world!\n");
run();
return 0;
}
// code.c
#include<stdio.h>
void run() {
printf("running...\n");
}
// 编译两个源⽂件
$ gcc -c hello.c
$ gcc -c code.c
$ ls
code.c code.o hello.c hello.o
可以看到,在编译之后会生成两个扩展名为.o 的文件,它们被称为目标文件。要注意的是如果我们修改了一个源文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制文件,文件的格式是ELF,是对二进制代码的一种封装。
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
## file命令⽤于辨识⽂件类型
6.ELF文件
要理解编译链接的细节,我们就不得不了解一下ELF文件。其实有以下四种文件其实都是ELF文件:
- 可重定位文件(Relocatable File):即xxx.o 文件。包含适合于其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
- 可执行文件(Executable File):即可执行程序。
- 共享目标文件(Shared Object File):即xxx.so 文件。
- 内核转储(core dumps),存放当前进程的执行上下文,用于dump信号触发。
一个ELF文件由以下四部分组成:
- ELF头(ELF header):描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。
- 程序头表(Program header table):列举了所有有效的段(segments)和他们的属性。 表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分割开。
- 节头表(Section header table):包含对节(section)的描述。
- 节(Section):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都储存在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。
因为节的存在,这也使得OS可以实现分批加载,即以节为单位进行加载,需要哪一块节就加载哪个一块节,不再将整个ELF文件全部加载进内存,节省了空间
最常见的节
- 代码节(.text):用于保存机器指令,是程序的主要执行部分。
- 数据节(.data):保存已初始化的全局变量和局部静态变量。
咱们也可以通过输入 size 可执行程序名 进行查看
7. ELF 从形成到加载轮廓
7.1 ELF形成可执行
- step -1:将多份C/C++ 源代码,翻译成为目标 .o 文件
- step -2:将多份 .o 文件进行链接。
注: 链接实际上就是将我们的一个一个的相同属性的section进行合并!
那么,在文件内部,它是如何知道哪一份区域是什么呢?
对于任何一个文件,文件的内容就是一个巨大的 “一维数组”,标识文件任何一个区域,都是通过 偏移量 + 大小 的方式。
注意:
实际合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库合并,此处不做过多追究 感兴趣可以看看这篇文章 : ELF 文件合并底层过程的深入研究与分析-CSDN博客
7.2 ELF可执行文件加载
- 一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
- 合并原则:相同属性,⽐如:可读,可写,可执⾏,需要加载时申请空间等.
- 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到⼀起
- 很显然,这个合并⼯作也已经在形成ELF的时候,合并⽅式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table) 中
# 查看可执行程序的sectino
[yang@hcss-ecs-b7e7 lesson7]$ readelf -S a.out
There are 30 section headers, starting at offset 0x19c8:
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
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
0000000000000038 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002d0 000002d0
0000000000000138 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400408 00000408
0000000000000092 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 000000000040049a 0000049a
000000000000001a 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000004004b8 000004b8
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 00000000004004d8 000004d8
0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004004f0 000004f0
00000000000000a8 0000000000000018 AI 5 23 8
[11] .init PROGBITS 0000000000400598 00000598
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004005c0 000005c0
0000000000000080 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400640 00000640
0000000000000232 0000000000000000 AX 0 0 16
...
#查看section合并的segment
[yang@hcss-ecs-b7e7 lesson7]$ readelf -l a.out
Elf file type is EXEC (Executable file)
Entry point 0x400640
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000009d4 0x00000000000009d4 R E 200000
LOAD 0x0000000000000e00 0x0000000000600e00 0x0000000000600e00
0x0000000000000254 0x0000000000000258 RW 200000
DYNAMIC 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
0x00000000000001e0 0x00000000000001e0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000008ac 0x00000000004008ac 0x00000000004008ac
0x0000000000000034 0x0000000000000034 R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e00 0x0000000000600e00 0x0000000000600e00
0x0000000000000200 0x0000000000000200 R 1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
- 注: readelf是Linux系统下用于查看ELF 文件详细信息的工具。
- -h:显示ELF文件头信息。包括文件类型(如可执行文件、共享目标文件、核心文件)、目标处理器架构(如x86、ARM)、入口点地址(程序开始执行的内存地址)等。
- -S:展示节区头表(Section Header Table)的信息。可以查看文件中的节区,像代码节(.text)、数据节(.data)、符号表节(.symtab)等的相关细节,如节区的大小、起始地址等。
- -s:用于查看符号表(Symbol Table)。能够显示程序中的符号信息,包含函数名、变量名,以及它们对应的地址和大小等内容。
- -l:显示程序头表(Program Header Table)。该选项可以帮助你理解文件在内存中的布局情况,如段的加载地址、大小和属性。
- -r:展示重定位节(Relocation Sections)的信息。重定位信息对于链接器在将不同目标文件组合成可执行文件或共享库时非常重要。
为什么要将section合并成为segment
- Section合并的主要原因是为了减少⻚⾯碎⽚,提⾼内存使⽤效率。如果不进⾏合并,假设⻚⾯⼤⼩为4096字节(内存块基本⼤⼩,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占⽤3个⻚⾯,⽽合并后,它们只需2个⻚⾯。
- 此外,操作系统在加载程序时,会将具有相同属性的section合并成⼀个⼤的segment,这样就可以实现不同的访问权限,从⽽优化内存管理和权限访问控制。
对于 程序头表 和 节头表 ⼜有什么⽤呢,其实 ELF ⽂件提供 2 个不同的视图/视⻆来让我们理解这
两个部分:
- 链接视图(Linking view) - 对应节头表 Section header table
- ⽂件结构的粒度更细,将⽂件按功能模块的差异进⾏划分,静态链接分析的时候⼀般关注的是链接视图,能够理解 ELF ⽂件中包含的各个部分的信息。
- 为了空间布局上的效率,将来在链接⽬标⽂件时,链接器会把很多节(section)合并,规整成可执⾏的段(segment)、可读写的段、只读段等。合并了后,空间利⽤率就⾼了,否则,很⼩的很⼩的⼀段,未来物理内存⻚浪费太⼤(物理内存⻚分配⼀般都是整数倍⼀块给你,⽐如4k),所以,链接器趁着链接就把⼩块们都合并了。
- 执⾏视图(execution view) - 对应程序头表 Program header table
- 告诉操作系统,如何加载可执⾏⽂件,完成进程内存的初始化。⼀个可执⾏程序的格式中,⼀定有 program header table 。
- 说⽩了就是:⼀个在链接时作⽤,⼀个在运⾏加载时作⽤。
从 链接视图 来看:
- 命令 readelf -S hello.o 可以帮助查看ELF文件的 节头表。
- .text节:是保存了程序代码指令的代码节。
- .data节:保存了初始化的全局变量和局部静态变量等数据。
- .rodata节:保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能在text段(不是data段)中找到.rodata节。
- .BSS节:为未初始化的全局变量和局部静态变量预留位置。
- .symtab节:Symbol Table 符号表,就是源码里面那些函数名、变量名和代码的对应关系。
- .got.plt节(全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接库在运行时进行修改。对于GOT的理解,我们后面会说。
- 使用readelf命令查看.so文件可以看到该节。
从 执行视图 来看:
- 告诉操作系统哪些模块可以被加载进内存。
- 加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。
我们可以在 ELF头 中找到问价的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。例如我们查看下hello.o这个可重定位文件的主要信息:
// 查看⽬标⽂件
$ readelf -h hello.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) # 指出ELF⽂件的类型
Machine: Advanced Micro Devices X86-64 # 该程序需要的体系结构
Version: 0x1
Entry point address: 0x0 # 系统第⼀个传输控制的虚拟地址,在那启动进程。假如⽂件没有如何关联的⼊⼝点,该成员就保持为0。
Start of program headers: 0 (bytes into file)
Start of section headers: 728 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes) # 保存着ELF头⼤⼩(以字节计数)
Size of program headers: 0 (bytes) # 保存着在⽂件的程序头表
(program header table)中⼀个⼊⼝的⼤⼩
Number of program headers: 0 # 保存着在程序头表中⼊⼝的个
数。因此,e_phentsize和e_phnum的乘积就是表的⼤⼩(以字节计数).假如没有程序头表,变量为0。
Size of section headers: 64 (bytes) # 保存着section头的⼤⼩(以字节计数)。⼀个section头是在section头表的⼀个⼊⼝
Number of section headers: 13 # 保存着在section header
table中的⼊⼝数⽬。因此,e_shentsize和e_shnum的乘积就是section头表的⼤⼩(以字节计数)。
假如⽂件没有section头表,值为0。
Section header string table index: 12 # 保存着section名字字符表相关⼊⼝的section头表(section header table)索引。
// 查看可执⾏程序
$ gcc *.o
$ readelf -h a.out
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: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060
Start of program headers: 64 (bytes into file)
Start of section headers: 14768 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
对于 ELF HEADER 这部分来说,我们只用知道其作用即可,它的主要目的是定位文件的其他部分。
8. 理解链接与加载
8.1 静态链接
无论是自己的.o,还是静态库中的.o,本质都是把.o文件进行链接的过程。
所以:研究静态链接,本质上就是研究.o文件是如何链接的
测试代码如下:
查看编译后的.o目标文件
- objdump 是一个在linux系统下用于反汇编和查看目标文件信息的工具
- 反汇编代码:能把目标文件(如可执行文件、共享库、目标代码文件)中的机器码反汇编成汇编语言代码,方便理解程序的逻辑。例如,对于一个简单的C程序编译后的可执行文件,使用objdump -d可以查看。
- 查看文件头信息:可以显示目标文件的文件头内容,包括文件格式(如ELF格式)、目标架构(如x86_64)等。例如,objdump -h可以展示文件各个节(section)的头部信息,像代码段(.text)、数据段(.data)的大小、在文件中的偏移量等。
- 查看符号表:符号表包含了程序中定义的函数名、全局变量名等信息。objdump -t能够查看这些符号相关的内容,如符号在内存中的地址等,有助于调试和理解程序的链接过程。
- D:对整个文件进行反汇编,不仅仅是代码段部分。
- S:尽可能将源代码与反汇编代码混合显示,这在分析程序时可以很直观地看到C代码和对应的汇编指令之间的关系。
- r:显示文件的重定位信息,用于了解目标文件在链接过程中的地址调整情况。
查看编译后.o目标文件
我们查看一下源文件
$ cat hello.c
#include<stdio.h>
void run();
int main()
{
printf("hello world!\n");
run();
return 0;
}
我们发现,hello.o文件中 main函数并不认识printf和run函数
$ cat code.c
#include<stdio.h>
void run()
{
printf("running...\n");
}
同理,code.o 也同样不认识printf函数
我们可以看到这里的call指令,它们分别对应之前调用的printf和run函数,但是你会发现他们的跳转地址都被设成了0,那这是为什么呢?
其实就是在编译hello.c的时候,编译器是完全不知道printf和run函数的存在的,比如它们位于内存的那个区块,代码长什么样都是不知道的。因此,编译器只能将这两个函数的跳转地址先暂时设为0
那么,这个地址会在什么时候被修正呢? 链接的时候!为了让链接器将来在链接时能够正确定位到这些被修正的地址,在代码块(.data)中还存在一个重定位表,这张表将来在链接的时候,就会根据表里记录的地址将其修正。
注: printf涉及到动态表链接,这里暂时不做说明
整个过程:
1. 查看code.o,hello.o 的代码段
至此就是之前的结论:多个.o文件彼此不知道对方
2. 读取code.o符号表
puts:就是printf的实现 UND/0:就是undefine,表示未定义说白了就是该符号在本文件中找不到
Ndx:就是Index 表示符号所在的节区索引,例如1 ,则表示该符号位于节区头表索引为1的节区内。
ABS:表示绝对符号,这些符号通常有固定的地址,不依赖于节区。
3.读取hello.o 符号表
同理,这里显示 printf 和run 在hello.o
4.读取main.exe 符号表
将两个.o进行合并之后,在最终的可执行程序中,就找到了run
000000000040052d:其实是地址,后面解释
FUNC:表示run符号类型是个函数
13:就是run函数所在的section被最终合并到的那一个section,下标就是13
5.读取可执行程序最终的所有section清单
hello.o和code.o的.text被合并了,是main.exe的第13个section
那么怎么证明上面的说法呢?
关于 hello.o 或者code.o call后面的00 00 00 00 有没有被修改成为具体的最终函数地址呢?
我们使用 objdump -d main.exe 查看 main.exe 的函数反编译部分,我们就会发现
最终:
1. 两个.o文件的代码段合并到了一起,并进行了统一的编制
2. 链接的时候,会修改.o文件中没有确定的函数地址,在合并完成之后,进行相关call地址,完成代码调用
过程: 汇编为.o文件时,将本文件的函数代码汇编为二进制代码,并且将其中找不到的函数地址设为00 00 00 00,并在符号表中做标记,方便之后链接修改,链接之后,将.o文件中的临时地址重定位修正,并将相同类型的段合并,调整段的大小,位置等属性。
静态链接就是把库中的.o进行合并,和上述过程一样
所以,链接其实就是将编译之后的所有目标文件连同用到的一些静态库运行时的库组合,拼装成一个独立的可执行文件。其中就包括我们之前提到的地址修正,当所有模块组合在一起之后,链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这其实就是静态链接的过程。
所以,链接过程中会涉及到.o文件中的外部符号进行地址重定位。
8.2 ELF加载与进程地址空间
8.2.1 虚拟地址/逻辑地址
问题:
- 一个ELF程序,在没有被加载到内存的时候,有没有地址呢?
- 进程mm_struct|、vm_area_struct 在进程刚刚创建的时候,初始化的数据都是从哪里来的?
答案:
- 一个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采用 “平坦模式” 进行工作。所以也要求ELF对自己的代码和数据进行统一编址,下面是objdump -S 反汇编之后的代码
- 最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量),但是我们认为起始地址是0,也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执行程序进行统一编址了。
- 进程mm_struct、vm_area_struct 在进程刚刚创建的时候,初始化数据从哪里来的? 从ELF各个segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的 [start,end] 等范围数据,另外再用详细地址,填充页表。
所以:虚拟地址机制,不光光需要OS支持,编译器也需要支持!!
8.2.2 重新理解进程虚拟地址空间
ELF在被编译好之后,会把自己未来程序的入口地址记录在ELF hander的Entry字段中:
接下来,我们来重新再谈一下文件底层执行的过程,
首先,OS 通过文件系统在磁盘上找到所对应文件的位置,通过各个节的偏移量,分别将其读取到物理内存上特定的区域上,之后通过页表,使其进程结构体中的内存结构体(实际上就也就是维护一个虚拟地址空间的结构体)与物理地址产生映射关系,此时,CPU工作过程为,执行OS的指令,而指令携带的是虚拟地址,所以执行程序的时候,需要进行虚实转换,而虚实转换的过程就是当CPU通过PC/EIP 获取到指令地址时,由于这个地址是虚拟地址,而cr3寄存器在x86架构下,用于存储页目录的物理基地址。而MMU(主要负责虚拟地址到物理地址的转换,内存管理单元)利用cr3指向的页目录,以及相关的页表项就可以确定对应的物理地址,然后从物理内存中获取实际的指针内存返回给CPU执行。
那么为什么要有虚拟地址和虚拟地址空间呢?
这里需要注意下虚拟地址的定义:由程序所使用的地址空间,在程序执行时由CPU产生。虚拟地址在程序编译时就确定了,但在程序执行时需要通过地址映射机制转换成物理地址才能在内存中找到对应的数据。
这里可以看下这篇文章,虚拟地址与虚拟地址空间:计算机内存管理的基石-CSDN博客
接下来,我们谈论下mm_struct 内部是如何划分各个功能区域的
如上图,mm_struct 内部就是有一个vm_area_struct 的指针,而这个结构体,本质上是一个链表,每一个vm_area_sturct 都有一个start 用来表示自己所对应的区域的开始,end用来表示自己所对应的区域的结尾,以及一个next指向下一个区域,prev指向上一个区域,形象上就是上图所描述的那样
8.3 动态链接与动态库加载
8.3.1 进程如何看到动态库
如上图,.so库中内部实际也是采用 基地址 + 偏移量的方式来查找方法的,所以方法地址:本质就是在库中的偏移量,至于库是如何加载到内存中的,我们在后文会进行讲解
8.3.2 进程间如何共享库的
同样,只要当库加载到了物理内存中,各个进程就能够通过页表映射的方式来找到库的位置,这样也就实现了库的共享了
8.3.3 动态链接
8.3.3.1 概要
动态链接其实远比静态链接要常用的多。比如我们查看下hello这个可执行程序依赖的动态库,会发现他就用到了一个c的动态链接库
$ ldd hello
linux-vdso.so.1 => (0x00007fffeb1ab000)
libc.so.6 => /lib64/libc.so.6 (0x00007ff776af5000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff776ec3000)
# ldd命令⽤于打印程序或者库⽂件所依赖的共享库列表
这里的 libc.so 是C语言运行时的库,里面提供了常用的标准输入输出文件字符串处理等等这些功能。
那为什么编译器默认不使用静态链接呢?静态链接会将编译产生的所有目标文件,连同用到的各种库,合并形成一个独立的可执行文件,它不需要额外的依赖就可以运行。照理来说应该更加方便才对是吧?
静态链接最大的问题在于生成的文件体积大,并且相当耗费内存资源。随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能包含了相同的功能和代码,显然会浪费大量的硬盘空间。
这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成一个独立的动态链接库,等到程序运行的时候再将它们加载到内存,这样不但可以节省空间,因为同一个模块在内存中只需要保留一份副本,可以被不同的进程所共享。
那么,动态链接到底是如何工作的??
首先要交代一个结论,动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连通它用到的一系列动态库先加载到内存,其中动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。
当动态库被加载到内存以后,一旦它的内存地址被确定,我们就可以去修正动态库中那些函数跳转的地址了。
8.3.3.2 我们的可执行程序被编译器动了手脚
通过上图,我们发现,为什么两个程序链接的是同一个库,可是库的位置却不一样呢?
那么,在回答这个问题之前,我们先了解下,程序在开始时的执行过程
在C/C++ 程序中,当程序开始执行时,它首先并不会直接跳转到 main 函数。 实际上,程序的入口点 _start , 这是一个由C运行时的库(通常是glibc)或者链接器(如ld)提供的特殊函数。
在_start 函数中,会执行一系列初始化操作,这些操作包括:
1. 设置堆栈:为程序创建一个初始的堆栈环境。
2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。
3. 动态链接:这是关键的一步,_start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。
动态链接器:
- 动态链接器(如ld-linux.so)负责在程序运行时加载动态库。
- 当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。
环境变量和配置文件:
- Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf 及其子配置文件)来指定动态库的搜索路径。
- 这些路径会被动态链接器在加载动态库时搜索。
缓存文件:
- 为了提高动态库的加载效率,Linux系统会维护一个名为/etc/ld.so.cache的缓存文件
- 该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个文件。
4 .调用 __libc_start_main :一旦动态链接完成,_start 函数会调用 __libc_start_main(这时glibc提供的一个函数)。__libc_start_main 函数负责执行一些额外的初始化工作,比如设置信号处理函数、初始化线程库(如果使用了线程)等。
5. 调用main函数:最后,__libc_start_main 函数会调用程序的main大安区,此时程序的执行控制权才正式交给用户编写的代码。
6. 处理main函数的返回值:当main函数返回时,__libc_start_main 会负责处理这个返回值,并最终调用 _exit 函数来终止程序。
上述过程描述了C/C++程序在 main 函数之前执行的一系列操作,但这些操作对于大多数程序员来说是透明的。程序员通常只需要关注 main 函数中的代码,而不需要关心底层的初始化过程。然而,了解这些底层细节有助于更好地理解程序的执行流程和调试问题。
8.3.3.3 动态库中的相对地址
动态库为了随时进行加载,为了支持并映射到任意进程的任意位置,对动态库中的方法,统一编址,采用相对编址的方案进行编制的(其实可执行程序也是一样的,都要遵守平坦模式,只不过exe是直接加载的 )。
# ubuntu下查看任意⼀个库的反汇编
objdump -S /lib/x86_64-linux-gnu/libc-2.31.so | less
# Cetnos下查看任意⼀个库的反汇编
$ objdump -S /lib64/libc-2.17.so | less
8.3.3.4 我们的程序,怎么和库具体映射起来的
注意:
- 动态库也是一个文件,要访问也是要被先加载,要加载也是要被打开的。
- 让我们的进程找到动态库的本质:也是文件操作,不过我们访问库函数,通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中
8.3.3.5 我们的程序,怎么进行库函数调用
注意:
- 库已经被我们映射到了当前进程的地址空间中
- 库的虚拟起始地址我们也已经知道了。
- 库中每一个方法的偏移量地址我们也知道
- 所有:访问库中任意位置,只需要知道库的起始虚拟地址+方法偏移量 即可定位库中的方法
- 而且:整个调用过程,是从代码区跳转到共享区,调用完毕再返回到代码区,整个过程完全在进程地址空间中进行的。
8.3.3.6 全局偏移量表GOT(global offset table)
注意:
- 也就是说,我们的程序运行之前,先把所有库加载并映射,所有库的起始虚拟地址都应该提前知道
- 然后对我们加载到内存中的程序的库函数调用进行地址修改,在内存中二次完成地址设置(这个叫做加载地址重定位)
- 等等,修改的是代码区吗?
- 不是说代码区在进程中是只读的吗? 怎么修改?能修改吗?
所以,动态链接采用的做法是 .data (可执行程序或者库自己)中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表GOT,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。
因为 .data 区域是可读写,所以可以支持动态进行修改
$ readelf -S a.out
...
[24] .got PROGBITS 0000000000003fb8 00002fb8
0000000000000048 0000000000000008 WA 0 0 8
...
$ readelf -l a.out # .got在加载的时候,会和.data合并成为⼀个segment,然后加载在⼀起
...
05 .init_array .fini_array .dynamic .got .data .bss
...
1. 由于代码段只读,我们不能直接修改代码段,但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对地址都不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表。
2. 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利用CPU的相对寻址来找到GOT表。
3.在调用函数的时候会首先查表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的地址。
4. 这种方法实现的动态链接就被叫做 PIC 地址无关代码 。 换句话说,我们动态库不需要被任何修改,没加载到任何内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=相对编制+GOT。
$ objdump -S a.out
...
0000000000001050 <puts@plt>:
1050: f3 0f 1e fa endbr64
1054: f2 ff 25 75 2f 00 00 bnd jmpq*0x2f75(%rip) #3fd0 <puts@GLIBC_2.2.5
...
...
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 8d 3d ac 0e 00 00 lea 0xeac(%rip),%rdi #
2004 <_IO_stdin_used+0x4>
1158: e8 f3 fe ff ff callq 1050 <puts@plt>
...
库的加载
因为动态链接器做的工作就是解析和加载动态库,这里对于动态链接器就不再赘述,可以看下这篇文章 动态链接的步骤 写的很详细
也可以配合着这张图来理解
我们在上面对于a.out 的代码反编译中看到其中有一条指令为 callq 1050 <puts@plt> ,跳转到1050 的位置,puts是printf的实现,这个我们能够理解,可是plt是什么呢?
8.3.3.7 库间依赖
注意:
不仅仅有可执行程序会掉用库,库也会调用其他库!! 库之间是有依赖的,如何做到库和库之间互相调用也是与地址无关的呢??
库中也有.GOT,和可执行一样! 这也就是大家为什么都是ELF格式的原因!
由于GOT表中的映射地址会在运行时去修改,我们可以通过gdb调试去观察GOT表的地址变化。在这里我们只用知道原理即可,有兴趣的以这篇文章作为参考通过GDB学透PLT与GOT_gdb调试 got-CSDN博客
- 由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步显然是非常耗时的。为了进一步降低开销,我们的操作系统还做了一些其他的优化,比如延迟绑定,或者也叫PLT(过程链接表 (Procedure Linkage Table)) 。与其在程序一开始就对所有函数进行重定位,不如将这个过程推迟到函数第一次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被用到,
思路:GOT中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码/stup。在我们第一次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。
总而言之,动态链接实际上将链接的整个过程,比如符号查询、地址的重定位从编译时推迟到了程序的运行时,他虽然牺牲了一定的性能和程序加载时间,但绝对是物有所值的。因为动态链接能够更有效的利用磁盘空间和内存资源,以极大方便了代码的更新和维护,更关键的是,它实现了二进制级别的代码复用。
注: 解析依赖关系的时候,就是加载并完善互相之间的GOT表的过程。
8.3.4 总结
- 静态链接的出现,提高了程序的模块化水平。对于一个大的项目,不同的人可以独立地测试和开发自己的模块。通过静态链接,生成最终的可执行文件。
- 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并成为一个独立的可执行文件,其中我们会去修正模块间函数的跳转地址,也叫做编译重定位(也叫做静态重定位)。
- 而动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间,然后通过.GOT 方式进行调用(运行重定位,也叫做动态地址重定位)。
附录:操作大全
附录1: ELF结构
附件2:查看ELF Header
-h或 --file-header :显示ELF文件的文件头信息。文件头包含了ELF文件的基本信息,比如文件类型、机器类型、版本、入口点地址、程序头表和节头表的位置和大小等
# readelf -h main
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: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400640
Start of program headers: 64 (bytes into file)
Start of section headers: 7048 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
// 内核中关于ELF Header相关的数据结构
// 没错,操作系统⾃⼰必须能够识别特定格式的可执⾏程 序:/linux/include/elf.h
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF "magic
number" */
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file
offset */
Elf64_Off e_shoff; /* Section header table file
offset */
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
附录3:ELF Promgram Header Table
-l 或 --program-headers :显示ELF文件的程序头部(也称为段头)信息。可执行文件在内存中的布局和加载过程非常重要。
readelf -l main.exe
Elf file type is EXEC (Executable file)
Entry point 0x400440
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000000075c 0x000000000000075c R E 200000
LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x0000000000000224 0x0000000000000228 RW 200000
DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
0x00000000000001d0 0x00000000000001d0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x0000000000000608 0x0000000000400608 0x0000000000400608
0x000000000000003c 0x000000000000003c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x00000000000001f0 0x00000000000001f0 R 1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
附录4. ELF Section Header Table
-S 或 --section-headers:显示ELF文件的节头信息。节头描述了ELF文件的各个节的起始地址、大小、标志等信息
$ readelf -S main.exe
There are 30 section headers, starting at offset 0x1960:
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
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
0000000000000060 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400318 00000318
000000000000003d 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400356 00000356
0000000000000008 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400360 00000360
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400380 00000380
0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400398 00000398
0000000000000048 0000000000000018 AI 5 23 8
[11] .init PROGBITS 00000000004003e0 000003e0
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400400 00000400
0000000000000040 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400440 00000440
0000000000000192 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004005d4 000005d4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004005e0 000005e0
0000000000000028 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 0000000000400608 00000608
000000000000003c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400648 00000648
0000000000000114 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600e10 00000e10
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600e18 00000e18
0000000000000008 0000000000000008 WA 0 0 8
[20] .jcr PROGBITS 0000000000600e20 00000e20
0000000000000008 0000000000000000 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000600e28 00000e28
00000000000001d0 0000000000000010 WA 6 0 8
[22] .got PROGBITS 0000000000600ff8 00000ff8
0000000000000008 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000601000 00001000
0000000000000030 0000000000000008 WA 0 0 8
[24] .data PROGBITS 0000000000601030 00001030
0000000000000004 0000000000000000 WA 0 0 1
[25] .bss NOBITS 0000000000601034 00001034
0000000000000004 0000000000000000 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 00001034
000000000000002d 0000000000000001 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 00001068
0000000000000618 0000000000000018 28 47 8
[28] .strtab STRTAB 0000000000000000 00001680
00000000000001d5 0000000000000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 00001855
0000000000000108 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
附录5: 查看具体的sectinos 信息
objdump -S main.exe
main.exe: file format elf64-x86-64
Disassembly of section .init:
00000000004003e0 <_init>:
4003e0: 48 83 ec 08 sub $0x8,%rsp
4003e4: 48 8b 05 0d 0c 20 00 mov 0x200c0d(%rip),%rax # 600ff8 <__gmon_start__>
4003eb: 48 85 c0 test %rax,%rax
4003ee: 74 05 je 4003f5 <_init+0x15>
4003f0: e8 3b 00 00 00 callq 400430 <__gmon_start__@plt>
4003f5: 48 83 c4 08 add $0x8,%rsp
4003f9: c3 retq
Disassembly of section .plt:
0000000000400400 <.plt>:
400400: ff 35 02 0c 20 00 pushq 0x200c02(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400406: ff 25 04 0c 20 00 jmpq *0x200c04(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40040c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400410 <puts@plt>:
400410: ff 25 02 0c 20 00 jmpq *0x200c02(%rip) # 601018 <puts@GLIBC_2.2.5>
400416: 68 00 00 00 00 pushq $0x0
40041b: e9 e0 ff ff ff jmpq 400400 <.plt>
0000000000400420 <__libc_start_main@plt>:
400420: ff 25 fa 0b 20 00 jmpq *0x200bfa(%rip) # 601020 <__libc_start_main@GLIBC_2.2.5>
400426: 68 01 00 00 00 pushq $0x1
40042b: e9 d0 ff ff ff jmpq 400400 <.plt>
0000000000400430 <__gmon_start__@plt>:
400430: ff 25 f2 0b 20 00 jmpq *0x200bf2(%rip) # 601028 <__gmon_start__>
400436: 68 02 00 00 00 pushq $0x2
40043b: e9 c0 ff ff ff jmpq 400400 <.plt>
Disassembly of section .text:
0000000000400440 <_start>:
400440: 31 ed xor %ebp,%ebp
400442: 49 89 d1 mov %rdx,%r9
400445: 5e pop %rsi
400446: 48 89 e2 mov %rsp,%rdx
400449: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
40044d: 50 push %rax
40044e: 54 push %rsp
40044f: 49 c7 c0 d0 05 40 00 mov $0x4005d0,%r8
400456: 48 c7 c1 60 05 40 00 mov $0x400560,%rcx
40045d: 48 c7 c7 3d 05 40 00 mov $0x40053d,%rdi
400464: e8 b7 ff ff ff callq 400420 <__libc_start_main@plt>
400469: f4 hlt
40046a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
0000000000400470 <deregister_tm_clones>:
400470: b8 3f 10 60 00 mov $0x60103f,%eax
400475: 55 push %rbp
400476: 48 2d 38 10 60 00 sub $0x601038,%rax
40047c: 48 83 f8 0e cmp $0xe,%rax
400480: 48 89 e5 mov %rsp,%rbp
400483: 77 02 ja 400487 <deregister_tm_clones+0x17>
400485: 5d pop %rbp
400486: c3 retq
400487: b8 00 00 00 00 mov $0x0,%eax
40048c: 48 85 c0 test %rax,%rax
40048f: 74 f4 je 400485 <deregister_tm_clones+0x15>
400491: 5d pop %rbp
400492: bf 38 10 60 00 mov $0x601038,%edi
400497: ff e0 jmpq *%rax
400499: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
00000000004004a0 <register_tm_clones>:
4004a0: b8 38 10 60 00 mov $0x601038,%eax
4004a5: 55 push %rbp
4004a6: 48 2d 38 10 60 00 sub $0x601038,%rax
4004ac: 48 c1 f8 03 sar $0x3,%rax
4004b0: 48 89 e5 mov %rsp,%rbp
4004b3: 48 89 c2 mov %rax,%rdx
4004b6: 48 c1 ea 3f shr $0x3f,%rdx
4004ba: 48 01 d0 add %rdx,%rax
4004bd: 48 d1 f8 sar %rax
4004c0: 75 02 jne 4004c4 <register_tm_clones+0x24>
4004c2: 5d pop %rbp
4004c3: c3 retq
4004c4: ba 00 00 00 00 mov $0x0,%edx
4004c9: 48 85 d2 test %rdx,%rdx
4004cc: 74 f4 je 4004c2 <register_tm_clones+0x22>
4004ce: 5d pop %rbp
4004cf: 48 89 c6 mov %rax,%rsi
4004d2: bf 38 10 60 00 mov $0x601038,%edi
4004d7: ff e2 jmpq *%rdx
4004d9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
00000000004004e0 <__do_global_dtors_aux>:
4004e0: 80 3d 4d 0b 20 00 00 cmpb $0x0,0x200b4d(%rip) # 601034 <_edata>
4004e7: 75 11 jne 4004fa <__do_global_dtors_aux+0x1a>
4004e9: 55 push %rbp
4004ea: 48 89 e5 mov %rsp,%rbp
4004ed: e8 7e ff ff ff callq 400470 <deregister_tm_clones>
4004f2: 5d pop %rbp
4004f3: c6 05 3a 0b 20 00 01 movb $0x1,0x200b3a(%rip) # 601034 <_edata>
4004fa: f3 c3 repz retq
4004fc: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400500 <frame_dummy>:
400500: 48 83 3d 18 09 20 00 cmpq $0x0,0x200918(%rip) # 600e20 <__JCR_END__>
400507: 00
400508: 74 1e je 400528 <frame_dummy+0x28>
40050a: b8 00 00 00 00 mov $0x0,%eax
40050f: 48 85 c0 test %rax,%rax
400512: 74 14 je 400528 <frame_dummy+0x28>
400514: 55 push %rbp
400515: bf 20 0e 60 00 mov $0x600e20,%edi
40051a: 48 89 e5 mov %rsp,%rbp
40051d: ff d0 callq *%rax
40051f: 5d pop %rbp
400520: e9 7b ff ff ff jmpq 4004a0 <register_tm_clones>
400525: 0f 1f 00 nopl (%rax)
400528: e9 73 ff ff ff jmpq 4004a0 <register_tm_clones>
000000000040052d <run>:
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: bf f0 05 40 00 mov $0x4005f0,%edi
400536: e8 d5 fe ff ff callq 400410 <puts@plt>
40053b: 5d pop %rbp
40053c: c3 retq
000000000040053d <main>:
40053d: 55 push %rbp
40053e: 48 89 e5 mov %rsp,%rbp
400541: bf fb 05 40 00 mov $0x4005fb,%edi
400546: e8 c5 fe ff ff callq 400410 <puts@plt>
40054b: b8 00 00 00 00 mov $0x0,%eax
400550: e8 d8 ff ff ff callq 40052d <run>
400555: b8 00 00 00 00 mov $0x0,%eax
40055a: 5d pop %rbp
40055b: c3 retq
40055c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400560 <__libc_csu_init>:
400560: 41 57 push %r15
400562: 41 89 ff mov %edi,%r15d
400565: 41 56 push %r14
400567: 49 89 f6 mov %rsi,%r14
40056a: 41 55 push %r13
40056c: 49 89 d5 mov %rdx,%r13
40056f: 41 54 push %r12
400571: 4c 8d 25 98 08 20 00 lea 0x200898(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry>
400578: 55 push %rbp
400579: 48 8d 2d 98 08 20 00 lea 0x200898(%rip),%rbp # 600e18 <__init_array_end>
400580: 53 push %rbx
400581: 4c 29 e5 sub %r12,%rbp
400584: 31 db xor %ebx,%ebx
400586: 48 c1 fd 03 sar $0x3,%rbp
40058a: 48 83 ec 08 sub $0x8,%rsp
40058e: e8 4d fe ff ff callq 4003e0 <_init>
400593: 48 85 ed test %rbp,%rbp
400596: 74 1e je 4005b6 <__libc_csu_init+0x56>
400598: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40059f: 00
4005a0: 4c 89 ea mov %r13,%rdx
4005a3: 4c 89 f6 mov %r14,%rsi
4005a6: 44 89 ff mov %r15d,%edi
4005a9: 41 ff 14 dc callq *(%r12,%rbx,8)
4005ad: 48 83 c3 01 add $0x1,%rbx
4005b1: 48 39 eb cmp %rbp,%rbx
4005b4: 75 ea jne 4005a0 <__libc_csu_init+0x40>
4005b6: 48 83 c4 08 add $0x8,%rsp
4005ba: 5b pop %rbx
4005bb: 5d pop %rbp
4005bc: 41 5c pop %r12
4005be: 41 5d pop %r13
4005c0: 41 5e pop %r14
4005c2: 41 5f pop %r15
4005c4: c3 retq
4005c5: 90 nop
4005c6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4005cd: 00 00 00
00000000004005d0 <__libc_csu_fini>:
4005d0: f3 c3 repz retq
Disassembly of section .fini:
00000000004005d4 <_fini>:
4005d4: 48 83 ec 08 sub $0x8,%rsp
4005d8: 48 83 c4 08 add $0x8,%rsp
4005dc: c3 retq
查看编译后的.o目标文件
在了解了ELF文件的基本格式之后,我们就能来看看靖国便以后产生的目标.o文件的细节了
objdump -d hello.o
hello.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 callq e <main+0xe>
e: b8 00 00 00 00 mov $0x0,%eax
13: e8 00 00 00 00 callq 18 <main+0x18>
18: b8 00 00 00 00 mov $0x0,%eax
1d: 5d pop %rbp
1e: c3 retq