带你手把手解读frejail沙盒源码(0.9.72版本) (六)fldd

发布时间:2023年12月18日
├── fldd
│   ├── Makefile
│   └── main.c

功能概述

这段代码是一个简单的C程序,主要用于解析ELF(可执行和可链接格式)文件,并输出其中包含的库文件列表。这个程序主要由以下几个部分组成:

  1. 定义了一些全局变量和结构体。
  2. 实现了一些辅助函数,如storage_find()用于查找存储在链表中的元素,storage_add()用于向链表中添加元素等。
  3. parse_elf()函数是整个程序的核心,它接收一个ELF文件路径作为参数,然后打开该文件并将其映射到内存中。接着,它遍历ELF文件的各个节区,寻找动态链接器、运行路径和需要的库文件等信息,并将这些信息添加到相应的链表中。
  4. copy_libs_for_lib()函数负责查找给定库文件的实际路径,并递归地解析依赖的其他库文件。
  5. lib_paths_init()函数初始化默认的库搜索路径。
  6. walk_directory()函数遍历指定目录下的所有文件和子目录,对于每个找到的ELF文件,调用parse_elf()进行解析。
  7. usage()函数打印程序的使用说明。
  8. main()函数是程序的入口点,它处理命令行参数,调用上述函数完成任务。

总的来说,这个程序的功能是在给定的ELF文件或目录中找出所有依赖的库文件,并输出它们的路径。


ELF(Executable and Linkable Format)文件是一种通用的二进制文件格式,用于表示可执行程序、共享库、目标模块和其他类型的目标代码。一个完整的 ELF 文件通常包括以下部分:

  1. ELF 头(ELF Header):这是位于文件开头的一个固定大小的数据结构,描述了整个文件的基本信息,例如文件类型(可执行文件、对象文件等)、字节顺序、版本号、入口点地址以及段表和节表的位置等。

  2. 程序头表(Program Headers):这是一个可选的部分,包含了与进程映像相关的数据,如程序头部的数量、每个头部的大小以及每个头部的具体信息。这些头部定义了如何将文件映射到内存中,以便加载器能够正确地初始化进程。

  3. 节头表(Section Headers):这个部分是可选的,它描述了文件中的各个节,包括节的数量、每个节的名称、类型、大小、偏移量和链接信息等。节可以包含符号表、字符串表、重定位表、调试信息等。

  4. 节(Sections):这些是构成文件主体的各个数据区域,如代码段、数据段、BSS段(未初始化的数据段)以及其他一些特殊的节,如符号表、字符串表、动态链接表等。

  5. 数据:这是文件的主要内容,包括机器指令、静态数据、字符串常量、重定位信息、符号表条目等。

  6. 对齐填充(Padding):在某些情况下,为了满足特定硬件平台或操作系统的对齐要求,可能会在文件中添加一些无用的数据作为填充。

需要注意的是,并非所有的 ELF 文件都会包含所有这些部分。例如,可执行文件通常不包含节头表,而只包含程序头表。相反,对象文件(.o 文件)通常包含节头表,但不包含程序头表。


main.c


#include "../include/common.h"
#include "../include/ldd_utils.h"

#include <fcntl.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>


static int arg_quiet = 0;
static void copy_libs_for_lib(const char *lib);

typedef struct storage_t {
	struct storage_t *next;
	const char *name;
} Storage;
static Storage *libs = NULL;
static Storage *lib_paths = NULL;

// return 1 if found
static int storage_find(Storage *ptr, const char *name) {
	while (ptr) {
		if (strcmp(ptr->name, name) == 0)
			return 1;
		ptr = ptr->next;
	}

	return 0;
}

static void storage_add(Storage **head, const char *name) {
	if (storage_find(*head, name))
		return;

	Storage *s = malloc(sizeof(Storage));
	if (!s)
		errExit("malloc");
	s->next = *head;
	*head = s;
	s->name = strdup(name);
	if (!s->name)
		errExit("strdup");
}


static void storage_print(Storage *ptr, int fd) {
	while (ptr) {
		dprintf(fd, "%s\n", ptr->name);
		ptr = ptr->next;
	}
}

static bool ptr_ok(const void *ptr, const void *base, const void *end, const char *name) {
	bool r;
	(void) name;

	r = (ptr >= base && ptr < end);
	return r;
}


static void parse_elf(const char *exe) {
	int f;
	f = open(exe, O_RDONLY);
	if (f < 0) {
		if (!arg_quiet)
			fprintf(stderr, "Warning fldd: cannot open %s, skipping...\n", exe);
		return;
	}

	struct stat s;
	char *base = NULL, *end;
	if (fstat(f, &s) == -1)
		goto error_close;
	base = mmap(0, s.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, f, 0);
	if (base == MAP_FAILED)
		goto error_close;

	end = base + s.st_size;

	Elf_Ehdr *ebuf = (Elf_Ehdr *)base;
	if (strncmp((const char *)ebuf->e_ident, ELFMAG, SELFMAG) != 0) {
		if (!arg_quiet)
			fprintf(stderr, "Warning fldd: %s is not an ELF executable or library\n", exe);
		goto close;
	}

//遍历所有程序头 判断程序头类型
	Elf_Phdr *pbuf = (Elf_Phdr *)(base + sizeof(*ebuf));
	while (ebuf->e_phnum-- > 0 && ptr_ok(pbuf, base, end, "pbuf")) {
		switch (pbuf->p_type) {
		case PT_INTERP:
			// dynamic loader ld-linux.so
			if (!ptr_ok(base + pbuf->p_offset, base, end, "base + pbuf->p_offset"))
				goto close;

			storage_add(&libs, base + pbuf->p_offset);
			break;
		}
		pbuf++;
	}

	Elf_Shdr *sbuf = (Elf_Shdr *)(base + ebuf->e_shoff);
	if (!ptr_ok(sbuf, base, end, "sbuf"))
		goto close;

	// 遍历所有的节  Find strings section
	char *strbase = NULL;
	int sections = ebuf->e_shnum;
	while (sections-- > 0 && ptr_ok(sbuf, base, end, "sbuf")) {
		if (sbuf->sh_type == SHT_STRTAB) {
			strbase = base + sbuf->sh_offset;
			if (!ptr_ok(strbase, base, end, "strbase"))
				goto close;
			break;
		}
		sbuf++;   
	}
	if (strbase == NULL)
		goto error_close;

	// Find dynamic section
	sections = ebuf->e_shnum;
	while (sections-- > 0 && ptr_ok(sbuf, base, end, "sbuf")) {
// TODO: running fldd on large gui programs (fldd /usr/bin/transmission-qt)
// crash on accessing memory location sbuf->sh_type if sbuf->sh_type in the previous section was 0 (SHT_NULL)
// for now we just exit the while loop - this is probably incorrect
// printf("sbuf %p #%s#, sections %d, type %u\n", sbuf, exe, sections, sbuf->sh_type);
		if (!ptr_ok(sbuf, base, end, "sbuf"))
			goto close;

		if (sbuf->sh_type == SHT_NULL)
			break;
		if (sbuf->sh_type == SHT_DYNAMIC) {
			Elf_Dyn *dbuf = (Elf_Dyn *)(base + sbuf->sh_offset);
			if (!ptr_ok(dbuf, base, end, "dbuf"))
				goto close;
			// Find DT_RPATH/DT_RUNPATH tags first
			unsigned long size = sbuf->sh_size;
			while (size >= sizeof(*dbuf) && ptr_ok(dbuf, base, end, "dbuf")) {
				if (dbuf->d_tag == DT_RPATH || dbuf->d_tag ==  DT_RUNPATH) {
					const char *searchpath = strbase + dbuf->d_un.d_ptr;
					if (!ptr_ok(searchpath, base, end, "searchpath"))
						goto close;
					storage_add(&lib_paths, searchpath);
				}
				size -= sizeof(*dbuf);
				dbuf++;
			}
			// Find DT_NEEDED tags
			dbuf = (Elf_Dyn *)(base + sbuf->sh_offset);
			size = sbuf->sh_size;
			while (size >= sizeof(*dbuf) && ptr_ok(dbuf, base, end, "dbuf")) {
				if (dbuf->d_tag == DT_NEEDED) {
					const char *lib = strbase + dbuf->d_un.d_ptr;
					if (!ptr_ok(lib, base, end, "lib"))
						goto close;
					copy_libs_for_lib(lib);
				}
				size -= sizeof(*dbuf);
				dbuf++;
			}
		}
		sbuf++;
	}
	goto close;

 error_close:
	perror("copy libs");
 close:
	if (base)
		munmap(base, s.st_size);

	close(f);
}

static void copy_libs_for_lib(const char *lib) {
	Storage *lib_path;
	for (lib_path = lib_paths; lib_path; lib_path = lib_path->next) {
		char *fname;
		if (asprintf(&fname, "%s/%s", lib_path->name, lib) == -1)
			errExit("asprintf");
		if (access(fname, R_OK) == 0 && is_lib_64(fname)) {
			if (!storage_find(libs, fname)) {
				storage_add(&libs, fname);
				// libs may need other libs
				parse_elf(fname);
			}
			free(fname);
			return;
		}
		free(fname);
	}

	// log a  warning and continue
	if (!arg_quiet)
		fprintf(stderr, "Warning fldd: cannot find %s, skipping...\n", lib);
}

static void lib_paths_init(void) {
	int i;
	for (i = 0; default_lib_paths[i]; i++)
		storage_add(&lib_paths, default_lib_paths[i]);
}


static void walk_directory(const char *dirname) {
	assert(dirname);

	DIR *dir = opendir(dirname);
	if (dir) {
		struct dirent *entry;
		while ((entry = readdir(dir)) != NULL) {
			if (strcmp(entry->d_name, ".") == 0)
				continue;
			if (strcmp(entry->d_name, "..") == 0)
				continue;

			// build full path
			char *path;
			if (asprintf(&path, "%s/%s", dirname, entry->d_name) == -1)
				errExit("asprintf");

			// check regular so library
			char *ptr = strstr(entry->d_name, ".so");
			if (ptr && is_lib_64(path)) {
				if (*(ptr + 3) == '\0' || *(ptr + 3) == '.') {
					parse_elf(path);
					free(path);
					continue;
				}
			}

			char *rpath = realpath(path, NULL);
			if (!rpath) {
				free(path);
				continue;
			}
			free(path);

			struct stat s;
			if (stat(rpath, &s) == -1)
				errExit("stat");
			if (S_ISDIR(s.st_mode))
				walk_directory(rpath);
			free(rpath);
		}
		closedir(dir);
	}
}



static void usage(void) {
	printf("Usage: fldd program_or_directory [file]\n");
	printf("Print a list of libraries used by program or store it in the file.\n");
	printf("Print a list of libraries used by all .so files in a directory or store it in the file.\n");
}

int main(int argc, char **argv) {
#if 0
{
//system("cat /proc/self/status");
int i;
for (i = 0; i < argc; i++)
        printf("*%s* ", argv[i]);
printf("\n");
}
#endif
	if (argc < 2) {
		fprintf(stderr, "Error fldd: invalid arguments\n");
		usage();
		exit(1);
	}


	if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0) {
		usage();
		return 0;
	}

	warn_dumpable();

	// check program access
	if (access(argv[1], R_OK)) {
		fprintf(stderr, "Error fldd: cannot access %s\n", argv[1]);
		exit(1);
	}

	char *quiet = getenv("FIREJAIL_QUIET");
	if (quiet && strcmp(quiet, "yes") == 0)
		arg_quiet = 1;

	int fd = STDOUT_FILENO;
	// attempt to open the file
	if (argc == 3) {
		fd = open(argv[2], O_CREAT | O_TRUNC | O_WRONLY, 0644);
		if (fd == -1) {
			fprintf(stderr, "Error fldd: invalid arguments\n");
			usage();
			exit(1);
		}
	}

	// initialize local storage
	lib_paths_init();

	// process files
	struct stat s;
	if (stat(argv[1], &s) == -1)
		errExit("stat");
	if (S_ISDIR(s.st_mode))
		walk_directory(argv[1]);
	else {
		if (is_lib_64(argv[1]))
			parse_elf(argv[1]);
		else
			fprintf(stderr, "Warning fldd: %s is not a 64bit program/library\n", argv[1]);
	}


	// print libraries and exit
	storage_print(libs, fd);
	if (argc == 3)
		close(fd);
	return 0;
}

parse_elf

这段代码的主要目的是解析给定的 ELF 文件(exe),并提取其中的动态链接器、搜索路径和所需的库。以下是每一行代码的详细解释:

  1. static void parse_elf(const char *exe) {

定义一个名为 parse_elf 的静态函数,该函数接受一个指向字符串的指针(const char *exe),表示要处理的 ELF 文件名。

  1. int f;

声明一个整型变量 f,用于存储打开 ELF 文件时返回的文件描述符。

  1. f = open(exe, O_RDONLY);

使用 open() 函数以只读模式打开 ELF 文件,并将返回的文件描述符存储在 f 变量中。

  1. if (f < 0) {

检查 f 是否小于 0,如果小于 0,说明无法打开 ELF 文件。

  1. if (!arg_quiet)

检查全局变量 arg_quiet 是否为 0。如果为 0,则表示不启用静默模式,应该输出警告消息。

  1. fprintf(stderr, "Warning fldd: cannot open %s, skipping...\n", exe);

输出一条警告消息,说明无法打开指定的 ELF 文件,并跳过处理。

  1. return;

parse_elf 函数返回。

  1. }

结束 if 语句块。

  1. struct stat s;

声明一个 stat 结构体变量 s,用于存储 ELF 文件的状态信息。

  1. char *base = NULL, *end;

声明两个指向字符的指针变量 baseend,分别用于存储 ELF 文件映射到内存中的起始地址和结束地址。

  1. if (fstat(f, &s) == -1)

使用 fstat() 函数获取文件描述符 f 对应的文件状态信息,并将其存储在 s 中。如果失败,则返回 -1

  1. goto error_close;

跳转到 error_close 标签,执行错误处理。

  1. base = mmap(0, s.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, f, 0);

使用 mmap() 函数将 ELF 文件的内容映射到内存中,从偏移量 0 开始,大小为 s.st_size,权限为只读/写,映射类型为私有,文件描述符为 f。将映射区域的起始地址存储在 base 中。

  1. if (base == MAP_FAILED)

检查 base 是否等于 MAP_FAILED,如果是,则表示映射操作失败。

  1. goto error_close;

跳转到 error_close 标签,执行错误处理。

  1. end = base + s.st_size;

计算映射区域的结束地址,并将其存储在 end 中。

  1. Elf_Ehdr *ebuf = (Elf_Ehdr *)base;

创建一个指向 Elf_Ehdr 结构体的指针 ebuf,并将其初始化为 base 指向的地址。

在 C 语言中,类型转换是一种将一个变量从一种数据类型转换为另一种数据类型的方法。在这个上下文中,Elf_Ehdr *ebuf = (Elf_Ehdr *)base; 是一个类型转换的示例。

Elf_Ehdr 是一个预定义的结构体,它表示 ELF 文件的头部信息。而 base 是一个指向映射到内存中的 ELF 文件起始位置的指针。

(Elf_Ehdr *)base 是一个类型转换表达式,它将 base 指针转换为 Elf_Ehdr 类型的指针。这样做的目的是为了能够使用 base 指针来访问 ELF 文件头部的信息,因为这些信息是按照 Elf_Ehdr 结构体的布局存储的。

然后,这个转换后的指针被赋值给 Elf_Ehdr 类型的指针变量 ebuf。这样,我们就可以通过 ebuf 来访问和操作 ELF 文件的头部信息了。例如,我们可以使用 ebuf->e_ident 来访问头部的标识字段,或者使用 ebuf->e_phoff 来获取程序头表的偏移量等。

为什么base 使用char*

在 C 语言中,char * 类型的指针可以指向任何类型的内存区域。因此,在这个上下文中将 base 定义为 char * 类型的原因是,我们需要使用它来访问 ELF 文件的内容,而 ELF 文件的内容是由字节组成的。

mmap() 函数会将文件映射到进程的地址空间,并返回一个指向映射区域起始位置的指针。由于我们希望能够逐字节地读取和操作映射区域中的数据,所以选择 char * 类型作为指针类型是最合适的。

  1. if (strncmp((const char *)ebuf->e_ident, ELFMAG, SELFMAG) != 0) {

检查 ELF 文件头的标识字段是否与 ELF 格式的魔数相匹配。如果不匹配,则说明这不是一个有效的 ELF 文件。

这部分代码的作用是检查给定的文件是否是一个有效的 ELF 文件。在 ELF 格式的文件中,头部的第一个字段 e_ident 是一个 16 字节的数组,它包含了关于 ELF 文件的一些基本信息。

在这个数组的前四个字节(索引为 0、1、2 和 3)存储了魔数(Magic Number),也就是字符串 “ELF” 的 ASCII 码值。这个魔数用来快速地识别出一个文件是否是 ELF 格式。通常情况下,如果一个文件不是 ELF 格式,那么它的前四个字节就不会包含 “ELF” 这个字符串。

strncmp() 函数用于比较两个字符串的前 n 个字符是否相等。在这个上下文中,它是用来比较 ebuf->e_ident 数组的前四个字节(即魔数)和字符串 “ELF” 是否相等。ELFMAG 是一个预定义的宏,其值为 “ELF” 的 ASCII 码值,而 SELFMAG 则是这个字符串的长度(不包括结束符 ‘\0’),等于 4。

因此,如果 strncmp() 函数返回非零值,说明 ebuf->e_ident 数组的前四个字节与 “ELF” 不匹配,这意味着当前文件不是一个有效的 ELF 文件。

  1. if (!arg_quiet)

检查全局变量 arg_quiet 是否为 0。如果为 0,则表示不启用静默模式,应该输出警告消息。

  1. fprintf(stderr, "Warning fldd: %s is not an ELF executable or library\n", exe);

输出一条警告消息,说明指定的文件不是一个有效的 ELF 可执行文件或库。

  1. goto close;

跳转到 close 标签,执行关闭文件和释放内存的操作。

  1. }

结束 if 语句块。

  1. Elf_Phdr *pbuf = (Elf_Phdr *)(base + sizeof(*ebuf));

创建一个指向 Elf_Phdr 结构体的指针 pbuf,并将其初始化为 base 后面紧跟着 Elf_Ehdr 结构体大小的位置。

在 C 语言中,Elf_Phdr *pbuf = (Elf_Phdr *)(base + sizeof(*ebuf)); 是一个类型转换和指针运算的复合表达式。

首先,sizeof(*ebuf) 计算 Elf_Ehdr 结构体的大小。这里 *ebuf 表示一个指向 Elf_Ehdr 结构体的指针,所以 sizeof(*ebuf) 返回的是该结构体的大小(以字节为单位)。

然后,base + sizeof(*ebuf)base 指针向前移动 sizeof(*ebuf) 字节。因为 base 指针是 ELF 文件头部信息的起始地址,所以 base + sizeof(*ebuf) 的值就是 ELF 文件程序头表的起始地址。

最后,(Elf_Phdr *) 是一个类型转换操作,它将 (base + sizeof(*ebuf)) 转换为 Elf_Phdr * 类型的指针。这样我们就可以通过 pbuf 变量来访问和操作 ELF 文件的程序头表了。

总之,这个表达式的目的是计算并存储 ELF 文件程序头表的起始地址,并将其转换为 Elf_Phdr * 类型的指针。这样我们就可以方便地访问和操作程序头表中的数据了。

  1. while (ebuf->e_phnum-- > 0 && ptr_ok(pbuf, base, end, "pbuf")) {

开始一个循环,遍历 ELF 文件的所有程序头。每次迭代后,递减 ebuf->e_phnum 的值,直到其变为负数或不再满足条件 ptr_ok(pbuf, base, end, "pbuf")

  1. switch (pbuf->p_type) {

根据当前程序头的类型进行不同的处理。

  1. case PT_INTERP:

如果当前程序头是动态链接器(PT_INTERP)类型,则执行以下操作。

  1. // dynamic loader ld-linux.so

注释:这个程序头对应的是动态链接器 ld-linux.so

  1. if (!ptr_ok(base + pbuf->p_offset, base, end, "base + pbuf->p_offset"))

检查偏移量 base + pbuf->p_offset 是否位于映射区域内。

  1. goto close;

跳转到 close 标签,执行关闭文件和释放内存的操作。

  1. storage_add(&libs, base + pbuf->p_offset);

将动态链接器添加到全局变量 libs 中,作为依赖项。

  1. break;

结束 switch 语句。

  1. }

结束 case 语句。

  1. pbuf++;

pbuf 指针向前移动一个 Elf_Phdr 结构体的大小,以便处理下一个程序头。

  1. }

结束 while 循环。

  1. Elf_Shdr *sbuf = (Elf_Shdr *)(base + ebuf->e_shoff);

创建一个指向 Elf_Shdr 结构体的指针 sbuf,并将其初始化为 base 后面紧跟着 ebuf->e_shoff 字节的位置。

这行代码是 C 语言中的一种指针运算和类型转换的复合表达式,目的是获取 ELF 文件中的节头表(Section Headers)的起始地址,并将其存储在 sbuf 变量中。

  • 首先,(Elf_Shdr *)(base + ebuf->e_shoff) 是一个类型转换操作。它将 base + ebuf->e_shoff 的值转换为 Elf_Shdr * 类型的指针。这里的 base 是文件映射到内存中的起始地址,而 ebuf->e_shoff 是 ELF 头部中的节头表偏移量字段,表示从文件开始处到节头表起始位置的字节数。
  • 然后,这个表达式的计算结果被赋值给变量 sbuf。这样我们就可以通过 sbuf 指针来访问和操作 ELF 文件的节头表了。

需要注意的是,在进行这种类型转换时,我们需要确保转换后的指针仍然指向有效的内存区域,并且没有违反对齐要求。否则,使用这个指针可能会导致程序崩溃或者其他运行错误。

  1. if (!ptr_ok(sbuf, base, end, "sbuf"))

检查 sbuf 是否位于映射区域内。

  1. goto close;

跳转到 close 标签,执行关闭文件和释放内存的操作。

  1. char *strbase = NULL;

声明一个指向字符的指针变量 strbase,用于存储字符串表的起始地址。

  1. int sections = ebuf->e_shnum;

ebuf->e_shnum 存储在整型变量 sections 中,表示 ELF 文件中的段数量。

  1. while (sections-- > 0 && ptr_ok(sbuf, base, end, "sbuf")) {

开始一个循环,遍历 ELF 文件的所有节。每次迭代后,递减 sections 的值,直到其变为负数或不再满足条件 ptr_ok(sbuf, base, end, "sbuf")

  1. if (sbuf->sh_type == SHT_STRTAB) {

如果当前节是字符串表(SHT_STRTAB)类型,则执行以下操作。

  1. strbase = base + sbuf->sh_offset;

将字符串表的起始地址存储在 strbase 中。

strbase = base + sbuf->sh_offset; 这行代码的作用是获取 ELF 文件中的字符串表(.strtab)的起始地址,并将其存储在 strbase 变量中。

在处理动态段时,我们需要解析其中的 DT_NEEDED 标签,这个标签表示程序需要链接的共享库。但是,在动态段中,这些库的名字并不是直接以字符串的形式出现的,而是通过一个偏移量指向字符串表中的相应位置。因此,为了能够找到库文件的名字,我们需要知道字符串表的位置。

这就是为什么要在 strbase 变量中记录字符串表的起始地址的原因。有了这个地址,我们就可以根据每个 DT_NEEDED 条目的数据偏移量,计算出相应的字符串在字符串表中的位置,从而获取到库文件的名字。

总结一下,strbase = base + sbuf->sh_offset; 这行代码是为了后续处理动态段中的 DT_NEEDED 标签而设置的,它保存了字符串表的起始地址,以便于查找和解析库文件名。

这段程序的目的是查找 ELF 文件中的字符串表(.strtab),并将其起始地址存储在 strbase 变量中。字符串表是包含所有符号名和字符串数据的一个特殊节,它对于解析动态链接信息非常重要。

代码会遍历 ELF 文件的所有节头,并检查每个节头的类型是否为 SHT_STRTAB(表示字符串表)。如果找到一个字符串表,则将该节头的偏移量赋值给 strbase 变量,并跳过后续的节头检查。

关于您的问题:在正常情况下,ELF 文件只有一个字符串表,因此 strbase 变量不会被多个字符串表反复覆盖。但是,在某些特殊情况或自定义格式的文件中,可能会存在多个字符串表。在这种情况下,如果需要使用所有字符串表,那么可能需要修改代码来处理这种情况。

总之,这段代码的主要目的是获取 ELF 文件中的字符串表的起始地址,并将其存储在 strbase 变量中,以便于后续解析动态链接信息。

  1. if (!ptr_ok(strbase, base, end, "strbase"))

检查 strbase 是否位于映射区域内。

  1. goto close;

跳转到 close 标签,执行关闭文件和释放内存的操作。

  1. break;

结束 while 循环。

  1. }

结束 if 语句。

  1. sbuf++;

sbuf 指针向前移动一个 Elf_Shdr 结构体的大小,以便处理下一个节。

  1. }

结束 while 循环。

  1. if (strbase == NULL)

检查 strbase 是否为 NULL,如果是,则表示没有找到字符串表。

  1. goto error_close;

跳转到 error_close 标签,执行错误处理。

  1. sections = ebuf->e_shnum;

ebuf->e_shnum 存储在整型变量 sections 中,表示 ELF 文件中的段数量。

  1. while (sections-- > 0 && ptr_ok(sbuf, base, end, "sbuf")) {

开始一个循环,遍历 ELF 文件的所有节。每次迭代后,递减 sections 的值,直到其变为负数或不再满足条件 ptr_ok(sbuf, base, end, "sbuf")

  1. if (!ptr_ok(sbuf, base, end, "sbuf"))

检查 sbuf 是否位于映射区域内。

  1. goto close;

跳转到 close 标签,执行关闭文件和释放内存的操作。

  1. if (sbuf->sh_type == SHT_NULL)

如果当前节是空类型(SHT_NULL),则跳出循环。

  1. break;

结束 while

  1. if (sbuf->sh_type == SHT_DYNAMIC) {

检查当前节头的类型是否为动态段(SHT_DYNAMIC)。如果是,则执行接下来的代码。

  1. Elf_Dyn *dbuf = (Elf_Dyn *)(base + sbuf->sh_offset);

base 指针加上 sbuf->sh_offset 的值,得到动态段的起始地址,并将其转换为 Elf_Dyn * 类型的指针赋给 dbuf

  1. if (!ptr_ok(dbuf, base, end, "dbuf"))

使用 ptr_ok() 函数检查 dbuf 是否在有效的内存范围内。如果不在则跳转到 close 标签进行清理工作。

  1. unsigned long size = sbuf->sh_size;

将当前节的大小赋给变量 size

  1. while (size >= sizeof(*dbuf) && ptr_ok(dbuf, base, end, "dbuf")) {

开始一个循环,遍历动态段中的所有条目。每次迭代时检查剩余的大小是否大于等于 sizeof(*dbuf),并且 dbuf 在有效的内存范围内。

这行代码是一个 while 循环的条件判断部分,用于遍历动态段中的所有条目。接下来我将详细解释这个条件表达式。

size >= sizeof(*dbuf) 这部分是循环条件的核心部分。它表示只要当前剩余的大小(存储在变量 size 中)大于或等于一个 Elf_Dyn 结构体的大小,循环就会继续执行。其中:

  • size 是一个整型变量,用于记录当前节头中剩余未处理的字节数。
  • sizeof(*dbuf) 是一个表达式,返回指向 Elf_Dyn 结构体的指针所占的内存大小。在这里,*dbuf 表示一个 Elf_Dyn 结构体类型的值。

在这个循环中,每次迭代都会检查剩余的大小是否大于或等于一个 Elf_Dyn 结构体的大小。如果满足条件,则进入循环体内处理下一个条目。在循环体内,会更新剩余的大小,减去已处理的条目的大小。

这样做的目的是确保不会越界访问内存。因为在遍历动态段时,我们是从节头开始的,并且知道整个节的大小。通过比较剩余的大小和一个条目的大小,我们可以确定是否有足够的空间来安全地读取下一个条目。只有当有足够的空间时,才会进入循环体内处理条目。

总之,size >= sizeof(*dbuf) 这个条件是为了确保在遍历动态段时不会越界访问内存。通过检查剩余的大小是否足够读取下一个条目,可以保证程序的安全性。

  1. if (dbuf->d_tag == DT_RPATH || dbuf->d_tag == DT_RUNPATH) {

检查当前条目的标签是否为 DT_RPATH 或 DT_RUNPATH。这些标签用于指定库文件的搜索路径。

  1. const char *searchpath = strbase + dbuf->d_un.d_ptr;

根据当前条目的数据偏移量,计算出相应的字符串在字符串表中的位置,并将其赋给 searchpath

  1. if (!ptr_ok(searchpath, base, end, "searchpath"))

使用 ptr_ok() 函数检查 searchpath 是否在有效的内存范围内。如果不在则跳转到 close 标签进行清理工作。

  1. storage_add(&lib_paths, searchpath);

将找到的搜索路径添加到全局变量 lib_paths 中。

  1. }

结束 if 语句块。

  1. size -= sizeof(*dbuf);

更新剩余的大小,减去已处理的条目的大小。

  1. dbuf++;

dbuf 指针向前移动,指向下一个条目。

  1. }

结束 while 循环。

  1. // Find DT_NEEDED tags

注释:接下来查找 DT_NEEDED 标签,表示程序需要链接的共享库。

  1. dbuf = (Elf_Dyn *)(base + sbuf->sh_offset);

dbuf 重新初始化为指向动态段起始地址的指针。

  1. size = sbuf->sh_size;

将当前节的大小重新赋给变量 size

  1. while (size >= sizeof(*dbuf) && ptr_ok(dbuf, base, end, "dbuf")) {

开始一个新的循环,遍历动态段中的所有条目。

  1. if (dbuf->d_tag == DT_NEEDED) {

检查当前条目的标签是否为 DT_NEEDED。这个标签表示程序需要链接的共享库。

  1. const char *lib = strbase + dbuf->d_un.d_ptr;

根据当前条目的数据偏移量,计算出相应的字符串在字符串表中的位置,并将其赋给 lib

  1. if (!ptr_ok(lib, base, end, "lib"))

使用 ptr_ok() 函数检查 lib 是否在有效的内存范围内。如果不在则跳转到 close 标签进行清理工作。

  1. copy_libs_for_lib(lib);

调用 copy_libs_for_lib() 函数,将找到的库文件名传递给它。这个函数会递归地解析库文件的依赖关系,并将其添加到全局变量 libs 中。

  1. }

结束 if 语句块。

  1. size -= sizeof(*dbuf);

更新剩余的大小,减去已处理的条目的大小。

  1. dbuf++;

dbuf 指针向前移动,指向下一个条目。

  1. }

结束 while 循环。

  1. }

结束 if 语句块。

  1. sbuf++;

sbuf 指针向前移动,指向下一个节头。

  1. goto close;

跳转到 close 标签进行清理工作。

  1. error_close:
  2. perror("copy libs");

如果在处理动态段时发生错误,则输出错误信息。

  1. close:

标签用于关闭文件和释放映射的内存。

  1. if (base)

检查 base 是否为非空指针。如果是,则表示已经成功映射了 ELF 文件。

  1. munmap(base, s.st_size);

使用 munmap() 函数释放之前映射的内存。

  1. close(f);

关闭打开的 ELF 文件。

这部分代码的主要目的是解析 ELF 文件中的动态段,提取 DT_RPATH、DT_RUNPATH 和 DT_NEEDED 标签对应的字符串,并将其分别存储在 lib_pathslibs 变量中。这样就可以得到程序需要链接的所有共享库及其搜索路径。

请根据上面的程序说明一下 处理 ELF 头、程序表头和节头表 节 动态段起到的作用

在上面的程序中,处理 ELF 头、程序表头和节头表以及节的作用如下:

  1. ELF 头

    • 获取文件类型(可执行文件或共享库)。
    • 确定字节序,以便正确地解析文件中的数据。
    • 获取节头表的位置和大小。
  2. 程序表头

    • 可选部分,如果存在,则可以获取与进程映像相关的数据,如段的数量、每个段的大小以及每个段的具体信息。这些头部定义了如何将文件映射到内存中,以便加载器能够正确地初始化进程。
  3. 节头表

    • 提供文件中所有节的信息,包括节的名字、类型、大小、偏移量等。
    • 根据节头表中的信息,找到字符串表和其他重要节的位置。
    • 包含各种不同的数据,如机器指令、全局变量、符号表条目、字符串常量等。
    • 在解析动态段时,需要访问字符串表来获取库文件名等信息。
  4. 动态段

    • 包含与动态链接相关的信息,如 DT_NEEDED 标签对应的字符串、DT_RPATH 和 DT_RUNPATH 标签指定的库搜索路径等。
    • 解析动态段并提取相关的信息是最终的目标。

总的来说,处理这些部分是为了获取必要的信息,从而能够在后续步骤中正确地解析和使用 ELF 文件。例如,在解析动态段之前,我们需要知道动态段的位置,这可以通过节头表来获得。同样,我们还需要访问字符串表来获取库文件名等信息,这就需要通过节头表来定位字符串表。因此,处理这些部分为后续的工作提供了必要的上下文和基础信息。


ptr_ok()

这个函数 ptr_ok() 用于检查一个指针(ptr)是否在给定的内存区域范围内。它接受四个参数:

  1. const void *ptr:要检查的指针。
  2. const void *base:内存区域的起始地址。
  3. const void *end:内存区域的结束地址。
  4. const char *name:表示指针来源的字符串,主要用于调试信息。

函数首先声明了一个布尔变量 r,然后使用 (void) name; 来抑制编译器关于未使用 name 变量的警告。这是因为在这个实现中,name 参数并未被实际使用,但可能在其他实现或版本中会被用到。

接下来,函数通过比较 ptrbaseend 的值来确定 ptr 是否位于 [base, end) 范围内。如果 ptr >= base && ptr < end 成立,则说明 ptr 在指定的内存区域内,返回 true;否则,返回 false

这个函数通常用于验证从动态加载的库文件或其他外部源获取的指针是否有效。例如,在解析 ELF 文件时,我们需要确保我们访问的指针指向有效的数据结构,以防止发生越界访问或其他错误。


copy_libs_for_lib

这段代码定义了一个名为 copy_libs_for_lib 的函数,其作用是为给定的库文件(通过参数 lib 传递)递归地查找和添加依赖的其他库文件。下面是该函数各部分的详细解释:

  1. 声明局部变量

    • 定义一个类型为 Storage * 的指针变量 lib_path,用于遍历全局变量 lib_paths 中的搜索路径。
  2. 遍历搜索路径

    • 使用 for 循环遍历存储在全局变量 lib_paths 中的所有搜索路径。
    • 在每次循环迭代中,更新 lib_path 指针以指向下一个搜索路径。
  3. 生成完整的库文件名

    • 调用 asprintf() 函数,将当前搜索路径(由 lib_path->name 提供)和库名称(由参数 lib 提供)拼接成一个字符串,并将其分配到 fname 变量中。
    • 如果 asprintf() 函数返回值为 -1,则调用 errExit("asprintf") 函数并退出程序。
  4. 检查库文件是否存在且是 64 位格式

    • 使用 access() 函数检查库文件(由 fname 变量提供)是否存在且具有读权限。
    • 如果存在并且是 64 位格式(通过调用 is_lib_64(fname) 函数判断),则继续执行下面的操作。
  5. 检查库文件是否已经添加到全局变量 libs

    • 使用 storage_find(libs, fname) 函数检查库文件是否已经添加到了全局变量 libs 中。
    • 如果尚未添加,则调用 storage_add(&libs, fname) 函数将其添加到 libs 中。
    • 然后调用 parse_elf(fname) 函数递归地解析当前库文件的动态段,查找并添加更多的依赖库文件。
  6. 释放内存和结束循环

    • 使用 free(fname) 函数释放之前为库文件名分配的内存。
    • 结束循环,因为已经找到了一个可用的库文件。
  7. 处理找不到库文件的情况

    • 当遍历完所有的搜索路径仍未能找到可访问的库文件时,输出一条警告信息到标准错误流(如果未启用静默模式的话)。
  8. 结束函数

    • 函数结束,返回到调用者。

walk_directory

这是一个用C语言编写的函数,用于遍历指定目录下的所有子目录和文件。以下是该函数的每一行代码解释:

  1. static void walk_directory(const char *dirname) {:声明一个名为walk_directory的静态函数,接受一个指向字符串常量的指针作为参数。

  2. assert(dirname);:使用断言检查dirname是否为NULL,如果为NULL则程序会终止运行。

  3. DIR *dir = opendir(dirname);:使用opendir()函数打开dirname所指向的目录,并将返回的文件描述符存储在dir变量中。

  4. if (dir) {:检查dir是否非空(即成功打开了目录),如果是,则执行后续操作。

  5. struct dirent *entry;:定义一个dirent类型的结构体指针entry,用于保存读取到的目录项信息。

  6. while ((entry = readdir(dir)) != NULL) {:使用readdir()函数循环读取目录中的每一个条目,直到没有更多条目为止。

7-8. if (strcmp(entry->d_name, ".") == 0) continue;if (strcmp(entry->d_name, "..") == 0) continue;:跳过当前目录(“.”)和父目录(“…”)。

  1. char *path;:定义一个字符指针path,用于保存构建的完整路径。

  2. if (asprintf(&path, "%s/%s", dirname, entry->d_name) == -1):使用asprintf()函数创建一个完整的路径字符串,包括dirname和entry->d_name,并将其赋值给path。如果失败,调用errExit()函数并退出程序。

  3. // check regular so library:注释,表示接下来要检查是否为普通的.so库文件。

12-13. char *ptr = strstr(entry->d_name, ".so");if (ptr && is_lib_64(path)) {:查找entry->d_name中是否存在".so"字符串,并检查它是否是一个64位的库文件。

14-15. if (*(ptr + 3) == '\0' || *(ptr + 3) == '.') {parse_elf(path);:如果找到".so"后紧跟的是’\0’或’.',那么解析这个ELF库文件。

  1. free(path);:释放之前分配给path的内存。

walk_directory 函数中,我们看到有如下代码:

char *path;
if (asprintf(&path, "%s/%s", dirname, entry->d_name) == -1)
    errExit("asprintf");

// ...

char *rpath = realpath(path, NULL);
if (!rpath) {
    free(path);
    continue;
}
free(path);

在这段代码中,首先使用 asprintf() 函数动态地分配了一个字符串(path),用于存储当前目录项的完整路径。然后调用 realpath() 函数获取该路径的真实路径,并将其赋值给 rpath 变量。

由于 path 是通过 asprintf() 函数动态分配的内存,所以在不再需要它时,应该释放这部分内存以避免内存泄漏。因此,在调用 free(rpath) 之后,立即调用 free(path) 来释放之前为 path 分配的内存。

注意,即使 realpath() 失败并返回 NULL,也应该先释放 path 再继续执行循环。这是因为无论 realpath() 是否成功,都应该释放 path 占用的内存。

17-18. char *rpath = realpath(path, NULL);if (!rpath) {:使用realpath()函数获取path所指向的文件的实际路径,并将其赋值给rpath。如果返回NULL,释放path并继续下一个循环。

  1. free(path);:释放之前分配给path的内存。

20-21. struct stat s;if (stat(rpath, &s) == -1):定义一个stat结构体,然后使用stat()函数获取rpath所指向的文件的状态。如果出错,调用errExit()函数并退出程序。

22-23. if (S_ISDIR(s.st_mode)) walk_directory(rpath);:检查文件是否为目录,如果是,则递归地调用walk_directory()函数处理子目录。

  1. free(rpath);:释放之前分配给rpath的内存。

  2. }:结束while循环。

  3. closedir(dir);:关闭先前打开的目录。

  4. }:结束if语句块。

  5. }:结束walk_directory函数定义。

main 函数

这个 main 函数是整个程序的入口点,它负责处理命令行参数、执行相应的操作,并输出结果。下面是该函数各部分的详细解释:

  1. 检查参数数量

    • 如果传递给程序的参数数量少于 2,则打印错误信息并调用 usage() 函数显示使用帮助,然后退出程序。
  2. 检查是否请求帮助

    • 检查第一个参数(即 argv[1])是否为 “-h”、“–help” 或 “-?”,如果是则调用 usage() 函数显示使用帮助,并返回 0 以结束程序。
  3. 设置静默模式

    • 查找环境变量 “FIREJAIL_QUIET” 的值,如果找到且其值为 “yes”,则将全局变量 arg_quiet 设置为 1,启用静默模式。
  4. 打开文件或标准输出

    • 初始化文件描述符 fd 为标准输出。
    • 如果传递给程序的参数数量等于 3,则尝试打开第三个参数指定的文件,并将其赋值给 fd 变量。如果打开失败,则打印错误信息并调用 usage() 函数显示使用帮助,然后退出程序。
  5. 初始化本地存储结构

    • 调用 lib_paths_init() 函数初始化全局变量 lib_pathslibs

    lib_paths_init() 函数的主要作用是初始化一个链表 lib_paths,这个链表用于存储程序运行时需要搜索的默认库路径。

    以下是该函数执行过程的详细分析:

    1. 初始化一个循环变量 i 为0。
    2. 使用 for 循环遍历数组 default_lib_paths。这个数组包含了系统默认的库路径。
    3. 在循环中,使用 storage_add() 函数将当前的库路径添加到链表 lib_paths 中。如果链表中已经存在相同的库路径,则不再添加。
    4. 当遍历完所有默认库路径后,退出循环,完成链表 lib_paths 的初始化。

    总结来说,lib_paths_init() 函数主要是用来构建一个包含系统默认库路径的链表,以便在后续的操作中快速查找和加载所需的库文件。

  6. 处理文件

    • 使用 stat(argv[1], &s) 函数获取第一个参数指定的文件的信息,并将结果存储在结构体 s 中。
    • 如果文件是一个目录,则调用 walk_directory(argv[1]) 函数递归地遍历目录中的所有文件和子目录。
    • 否则,检查文件是否为 64 位共享库,如果是则调用 parse_elf(argv[1]) 函数解析库文件的动态段;否则,打印一条警告信息。

    walk_directory 函数中,当遍历目录时,它会检查每个文件名是否以 “.so” 结尾。如果是,则调用 parse_elf 函数来解析库文件的动态段。

    而在主函数中的另一个 else 语句块中,程序会检查命令行参数(即 argv[1])所指定的文件是否为 64 位共享库。如果是,则调用 parse_elf(argv[1]) 函数解析库文件的动态段。

    这两个地方都调用 parse_elf 函数的原因是它们分别处理两种不同情况下的库文件:

    • walk_directory 函数中,处理的是通过递归遍历目录树找到的所有库文件。
    • 在主函数的 else 语句块中,处理的是用户直接在命令行上指定的库文件。

    因此,这两种情况下都需要调用 parse_elf 函数来提取和记录库文件的依赖关系。

  7. 打印依赖库列表

    • 调用 storage_print(libs, fd) 函数打印已收集到的所有依赖库的列表。如果打开了一个文件,则将结果写入该文件中。
    • 如果之前打开了一个文件,则调用 close(fd) 关闭该文件。
  8. 结束程序

    • 返回 0,表示程序成功执行完毕。


ELF文件补充

前文结尾说到编译器编译源代码后生成的文件叫做目标文件,而目标文件经过编译器链接之后得到的就是可执行文件。那么目标文件到底是什么?它和可执行文件又有什么区别?链接到底又做了什么呢?接下来,我们将探索一下目标文件的本质。

目标文件的格式

目前,PC平台流行的 可执行文件格式(Executable) 主要包含如下两种,它们都是 COFF(Common File Format) 格式的变种。

  • Windows下的 PE(Portable Executable)
  • Linux下的 ELF(Executable Linkable Format)

目标文件就是源代码经过编译后但未进行链接的那些中间文件(Windows的.obj和Linux的.o),它与可执行文件的格式非常相似,所以一般跟可执行文件格式一起采用同一种格式存储。在Windows下采用PE-COFF文件格式;Linux下采用ELF文件格式。

事实上,除了可执行文件外,动态链接库(DDL,Dynamic Linking Library)静态链接库(Static Linking Library) 均采用可执行文件格式存储。它们在Window下均按照PE-COFF格式存储;Linux下均按照ELF格式存储。只是文件名后缀不同而已。

  • 动态链接库:Windows的.dll、Linux的.so
  • 静态链接库:Windows的.lib、Linux的.a

下面,我们将以ELF文件为例进行介绍。

ELF文件结构

在这里插入图片描述

注意:段(Segment)与节(Section)的区别。很多地方对两者有所混淆。段是程序执行的必要组成,当多个目标文件链接成一个可执行文件时,会将相同权限的节合并到一个段中。相比而言,节的粒度更小。

如图所示,为ELF文件的基本结构,其主要由四部分组成:

  • ELF Header
  • ELF Program Header Table (或称Program Headers、程序头)
  • ELF Section Header Table (或称Section Headers、节头表)
  • ELF Sections

从图中,我们就能看出它们各自的数据结构以及相互之间的索引关系。下面我们依次进行介绍。


ELF Header

我们可以使用readelf工具来查看ELF Header。

$ 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)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          672 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 10

ELF文件结构示意图中定义的Elf_Ehdr的各个成员的含义与readelf具有对应关系。如下表所示:

成员含义
e_identMagic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2’s complement, little end
Version: 1(current)
OS/ABI: UNIX - System V
ABI Version: 0
e_typeType: REL (Relocatable file)
ELF文件类型
e_machineMachine: Advanced Micro Devices X86-64
ELF文件的CPI平台属性
e_versionVersion: 0x1
ELF版本号。一般为常数1
e_entryEntry point address: 0x0
入口地址,规定ELF程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令。可重定位指令一般没有入口地址,则该值为0
e_phoffStart of program headers: 0(bytes into file)
e_shoffStart of section headers: 672 (bytes into file)
Section Header Table 在文件中的偏移
e_wordFlags: 0x0
ELF标志位,用来标识一些ELF文件平台相关的属性。
e_ehsizeSize of this header: 64 (bytes)
ELF Header本身的大小
e_phentsizeSize of program headers: 0 (bytes)
e_phnumNumber of program headers: 0
e_shentsizeSize of section headers: 64 (bytes)
单个Section Header大小
e_shnumNumber of section headers: 13
Section Header的数量
e_shstrndxSection header string table index: 10
Section Header字符串表在Section Header Table中的索引

ELF魔数

每种可执行文件的格式的开头几个字节都是很特殊的,特别是开头4个字节,通常被称为魔数(Magic Number)。通过对魔数的判断可以确定文件的格式和类型。如:ELF的可执行文件格式的头4个字节为0x7Felf;Java的可执行文件格式的头4个字节为cafe;如果被执行的是Shell脚本或perl、python等解释型语言的脚本,那么它的第一行往往是#!/bin/sh#!/usr/bin/perl#!/usr/bin/python,此时前两个字节#!就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序路径。

ELF文件类型

ELF文件主要有三种类型,可以通过ELF Header中的e_type成员进行区分。

  • 可重定位文件(Relocatable File)ETL_REL。一般为.o文件。可以被链接成可执行文件或共享目标文件。静态链接库属于可重定位文件。

  • 可执行文件(Executable File)ET_EXEC。可以直接执行的程序。

  • 共享目标文件(Shared Object File)

    ET_DYN
    

    。一般为

    .so
    

    文件。有两种情况可以使用。

    • 链接器将其与其他可重定位文件、共享目标文件链接成新的目标文件;
    • 动态链接器将其与其他共享目标文件、结合一个可执行文件,创建进程映像。

在这里插入图片描述

ELF Section Header Table

ELF 节头表是一个节头数组。每一个节头都描述了其所对应的节的信息,如节名、节大小、在文件中的偏移、读写权限等。编译器、链接器、装载器都是通过节头表来定位和访问各个节的属性的。

我们可以使用readelf工具来查看节头表。

$ readelf -S hello.o

There are 13 section headers, starting at offset 0x2a0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000015  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  000001f0
       0000000000000030  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000055
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  00000055
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  00000055
       000000000000000d  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  00000062
       0000000000000035  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000097
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  00000098
       0000000000000038  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000220
       0000000000000018  0000000000000018   I      11     8     8
  [10] .shstrtab         STRTAB           0000000000000000  00000238
       0000000000000061  0000000000000000           0     0     1
  [11] .symtab           SYMTAB           0000000000000000  000000d0
       0000000000000108  0000000000000018          12     9     8
  [12] .strtab           STRTAB           0000000000000000  000001d8
       0000000000000013  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

ELF文件结构示意图中定义的Elf_Shdr的各个成员的含义与readelf具有对应关系。如下表所示:

成员含义
sh_name节名
节名是一个字符串,保存在一个名为.shstrtab的字符串表(可通过Section Header索引到)。sh_name的值实际上是其节名字符串在.shstrtab中的偏移值
sh_type节类型
sh_flags节标志位
sh_addr节地址:节的虚拟地址
如果该节可以被加载,则sh_addr为该节被加载后在进程地址空间中的虚拟地址;否则sh_addr为0
sh_offset节偏移
如果该节存在于文件中,则表示该节在文件中的偏移;否则无意义,如sh_offset对于BSS 节来说是没有意义的
sh_size节大小
sh_link、sh_info节链接信息
sh_addralign节地址对齐方式
sh_entsize节项大小
有些节包含了一些固定大小的项,如符号表,其包含的每个符号所在的大小都一样的,对于这种节,sh_entsize表示每个项的大小。如果为0,则表示该节不包含固定大小的项。

节类型(sh_type)

节名是一个字符串,只是在链接和编译过程中有意义,但它并不能真正地表示节的类型。对于编译器和链接器来说,主要决定节的属性是节的类型(sh_type)和节的标志位(sh_flags)。

节的类型相关常量以SHT_开头,上述readelf -S命令执行的结果省略了该前缀。常见的节类型如下表所示:

常量含义
SHT_NULL0无效节
SHT_PROGBITS1程序节。代码节、数据节都是这种类型。
SHT_SYMTAB2符号表
SHT_STRTAB3字符串表
SHT_RELA4重定位表。该节包含了重定位信息。
SHT_HASH5符号表的哈希表
SHT_DYNAMIC6动态链接信息
SHT_NOTE7提示性信息
SHT_NOBITS8表示该节在文件中没有内容。如.bss
SHT_REL9该节包含了重定位信息
SHT_SHLIB10保留
SHT_DNYSYM11动态链接的符号表

节标志位(sh_flag)

节标志位表示该节在进程虚拟地址空间中的属性。如是否可写、是否可执行等。相关常量以SHF_开头。常见的节标志位如下表所示:

常量含义
SHF_WRITE1表示该节在进程空间中可写
SHF_ALLOC2表示该节在进程空间中需要分配空间。有些包含指示或控制信息的节不需要在进程空间中分配空间,就不会有这个标志。
SHF_EXECINSTR4表示该节在进程空间中可以被执行

节链接信息(sh_link、sh_info)

如果节的类型是与链接相关的(无论是动态链接还是静态链接),如**重定位表、符号表、**等,则sh_linksh_info两个成员所包含的意义如下所示。其他类型的节,这两个成员没有意义。

sh_typesh_linksh_info
SHT_DYNAMIC该节所使用的字符串表在节头表中的下标0
SHT_HASH该节所使用的符号表在节头表中的下标0
SHT_REL该节所使用的相应符号表在节头表中的下标该重定位表所作用的节在节头表中的下标
SHT_RELA该节所使用的相应符号表在节头表中的下标该重定位表所作用的节在节头表中的下标
SHT_SYMTAB操作系统相关操作系统相关
SHT_DYNSYM操作系统相关操作系统相关
otherSHN_UNDEF0

ELF Sections

节的分类

上述ELF Section Header Table部分已经简单介绍了节类型。接下来我们来介绍详细一些比较重要的节。

.text节

.text节是保存了程序代码指令的代码节一段可执行程序,如果存在Phdr,则.text节就会存在于text段中。由于.text节保存了程序代码,所以节类型为SHT_PROGBITS

.rodata节

rodata节保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能在text段(不是data段)中找到.rodata节。由于.rodata节是只读的,所以节类型为SHT_PROGBITS

.plt节(过程链接表)

.plt节也称为过程链接表(Procedure Linkage Table)其包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于.plt节保存了代码,所以节类型为SHT_PROGBITS

.data节

.data节存在于data段中,其保存了初始化的全局变量等数据。由于.data节保存了程序的变量数据,所以节类型为SHT_PROGBITS

.bss节

.bss节存在于data段中,占用空间不超过4字节,仅表示这个节本省的空间。.bss节保存了未进行初始化的全局数据。程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于.bss节未保存实际的数据,所以节类型为SHT_NOBITS

.got.plt节(全局偏移表-过程链接表)

.got节保存了全局偏移表.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。由于.got.plt节与程序执行有关,所以节类型为SHT_PROGBITS

.dynsym节(动态链接符号表)

.dynsym节保存在text段中。其保存了从共享库导入的动态符号表。节类型为SHT_DYNSYM

.dynstr节(动态链接字符串表)

.dynstr保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。

.rel.*节(重定位表)

重定位表保存了重定位相关的信息,这些信息描述了如何在链接或运行时,对ELF目标文件的某部分或者进程镜像进行补充或修改。由于重定位表保存了重定位相关的数据,所以节类型为SHT_REL

.hash节

.hash节也称为.gnu.hash,其保存了一个用于查找符号的散列表。

.symtab节(符号表)

.symtab节是一个ElfN_Sym的数组,保存了符号信息。节类型为SHT_SYMTAB

.strtab节(字符串表)

.strtab节保存的是符号字符串表,表中的内容会被.symtabElfN_Sym结构中的st_name引用。节类型为SHT_STRTAB

.ctors节和.dtors节

.ctors构造器)节和.dtors析构器)节分别保存了指向构造函数和析构函数的函数指针,构造函数是在main函数执行之前需要执行的代码;析构函数是在main函数之后需要执行的代码

符号表

节的分类中我们介绍了.dynsym节和.symtab节,两者都是符号表。那么它们到底有什么区别呢?存在什么关系呢?

符号是对某些类型的数据或代码(如全局变量或函数)的符号引用,函数名或变量名就是符号名。例如,printf()函数会在动态链接符号表.dynsym中存有一个指向该函数的符号项(以Elf_Sym数据结构表示)。在大多数共享库和动态链接可执行文件中,存在两个符号表。即.dynsym.symtab

.dynsym保存了引用来自外部文件符号的全局符号。如printf库函数。.dynsym保存的符号是.symtab所保存符合的子集,.symtab中还保存了可执行文件的本地符号。如全局变量,代码中定义的本地函数等。

既然.dynsym.symtab的子集,那为何要同时存在两个符号表呢?

通过readelf -S命令可以查看可执行文件的输出,一部分节标志位(sh_flags)被标记为了A(ALLOC)、WA(WRITE/ALLOC)、AX(ALLOC/EXEC)。其中,.dynsym被标记为ALLOC,而.symtab则没有标记。

ALLOC表示有该标记的节会在运行时分配并装载进入内存,而.symtab不是在运行时必需的,因此不会被装载到内存中。.dynsym保存的符号只能在运行时被解析,因此是运行时动态链接器所需的唯一符号.dynsym对于动态链接可执行文件的执行是必需的,而.symtab只是用来进行调试和链接的。

在这里插入图片描述

上图所示为通过符号表索引字符串表的示意图。符号表中的每一项都是一个Elf_Sym结构,对应可以在字符串表中索引得到一个字符串。该数据结构中成员的含义如下表所示:

成员含义
st_name符号名。该值为该符号名在字符串表中的偏移地址。
st_value符号对应的值。存放符号的值(可能是地址或位置偏移量)。
st_size符号的大小。
st_other0
st_shndx符号所在的节
st_info符号类型及绑定属性

使用readelf工具我们也能够看到符号表的相关信息。

$ readelf -s hello.o

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
     9: 0000000000000000    21 FUNC    GLOBAL DEFAULT    1 main
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

字符串表

类似于符号表,在大多数共享库和动态链接可执行文件中,也存在两个字符串表。即.dynstr.strtab,分别对应于.dynsymsymtab。此外,还有一个.shstrtab的节头字符串表,用于保存节头表中用到的字符串,可通过sh_name进行索引。

ELF文件中所有字符表的结构基本一致,如上图所示。

重定位表

重定位就是将符号定义和符号引用进行连接的过程。可重定位文件需要包含描述如何修改节内容的相关信息,从而使可执行文件和共享目标文件能够保存进程的程序镜像所需要的正确信息。

重定位表是进行重定位的重要依据。我们可以使用objdump工具查看目标文件的重定位表:

$ objdump -r hello.o


hello.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE
0000000000000005 R_X86_64_32       .rodata
000000000000000a R_X86_64_PC32     puts-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE
0000000000000020 R_X86_64_PC32     .text

重定位表是一个Elf_Rel类型的数组结构,每一项对应一个需要进行重定位的项。 其成员含义如下表所示:

成员含义
r_offset重定位入口的偏移。
对于可重定位文件来说,这个值是该重定位入口所要修正的位置的第一个字节相对于节起始的偏移
对于可执行文件或共享对象文件来说,这个值是该重定位入口所要修正的位置的第一个字节的虚拟地址
r_info重定位入口的类型和符号
因为不同处理器的指令系统不一样,所以重定位所要修正的指令地址格式也不一样。每种处理器都有自己的一套重定位入口的类型。
对于可执行文件和共享目标文件来说,它们的重定位入口是动态链接类型的。

重定位是目标文件链接成为可执行文件的关键。我们将在后面的进行介绍。

参考

  1. Executable and Linkable Format (ELF)
  2. 《Linux 二进制分析》
  3. 《深入理解计算机系统》
  4. 《程序员的自我修养——链接、装载与库》
  5. Executable and Linkable Format

接可执行文件中,也存在两个字符串表。即.dynstr.strtab,分别对应于.dynsymsymtab。此外,还有一个.shstrtab的节头字符串表,用于保存节头表中用到的字符串,可通过sh_name进行索引。

ELF文件中所有字符表的结构基本一致,如上图所示。

重定位表

重定位就是将符号定义和符号引用进行连接的过程。可重定位文件需要包含描述如何修改节内容的相关信息,从而使可执行文件和共享目标文件能够保存进程的程序镜像所需要的正确信息。

重定位表是进行重定位的重要依据。我们可以使用objdump工具查看目标文件的重定位表:

$ objdump -r hello.o


hello.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE
0000000000000005 R_X86_64_32       .rodata
000000000000000a R_X86_64_PC32     puts-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE
0000000000000020 R_X86_64_PC32     .text

重定位表是一个Elf_Rel类型的数组结构,每一项对应一个需要进行重定位的项。 其成员含义如下表所示:

成员含义
r_offset重定位入口的偏移。
对于可重定位文件来说,这个值是该重定位入口所要修正的位置的第一个字节相对于节起始的偏移
对于可执行文件或共享对象文件来说,这个值是该重定位入口所要修正的位置的第一个字节的虚拟地址
r_info重定位入口的类型和符号
因为不同处理器的指令系统不一样,所以重定位所要修正的指令地址格式也不一样。每种处理器都有自己的一套重定位入口的类型。
对于可执行文件和共享目标文件来说,它们的重定位入口是动态链接类型的。

重定位是目标文件链接成为可执行文件的关键。我们将在后面的进行介绍。

参考

  1. Executable and Linkable Format (ELF)
  2. 《Linux 二进制分析》
  3. 《深入理解计算机系统》
  4. 《程序员的自我修养——链接、装载与库》
  5. Executable and Linkable Format
文章来源:https://blog.csdn.net/qq_55125921/article/details/135065746
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。