linux C获取二进制文件的build-id

linux C获取二进制文件的build-id,第1张

linux C获取二进制文件的build-id 前言

build-id是gcc编译二进制文件的时候,计算得到的一个二进制文件的标识,有点类似文件哈希,可以用来判断文件是否一致(是否是同一版本的源码编译得到的)。
使用 file、readelf -n 等解析ELF文件的命令可以看到build-id:

通过查阅资料得知,build-id的值是保存在elf文件的**.note.gnu.build-id**段中,因此解析出elf文件中的该字段便可得到build-id:

关于build-id的简单描述(详细可以自行搜索):

解决方案 网上的一些思路

1)github上有一个项目,可以遍历获得二进制文件加载的动态链接文件,然后输出这些文件的build-id,不过如果链接的动态链接文件是一个软连接则不能获得build-id,因为它是直接解析了软连接文件,项目地址如下:
https://github.com/mattst88/build-id
这个项目核心就是用了dl_iterate_phdr获得链接库的文件信息结构

2)stackoverflow上有个项目,可以获取二进制文件自身的build-id,链接如下:
https://stackoverflow.com/questions/55641889/access-build-id-at-runtime
核心思路是用弱符号,在编译的时候该弱符号会被真正的.note.gnu.build-id值替换,所以直接打印该弱符号即可得到自身的build-id:

char build_id __attribute__((section(".note.gnu.build-id"))) = '!';

3)stackoverflow上还有一个项目,可以获取任意指定ELF文件的build-id,链接如下:
https://stackoverflow.com/questions/17637745/can-a-program-read-its-own-elf-section
核心思路是以二进制格式打开ELF文件,然后解析文件的section,获取.note.gnu.build-id节的内容(NT_GNU_BUILD_ID类型对应的内容),解析ELF文件代码实现参考了**readelf.c**。

最终方案

最后,结合我的需求,我选择了上面的方案三,在方案三的基础上做出了如下方案:
1)dladdr自动获取目标动态链接库文件的保存目录:

int __attribute__((weak)) __libc_start_main(); // __libc_start_main为libc.so中的符号, 如果想查看其它动态链接库,则定义一个只在目标动态链接库出现的弱符号函数即可
··· ···
Dl_info dl_info;
if(dladdr((void*)__libc_start_main, &dl_info) == 0) { // __libc_start_main为libc.so中的函数
    return FALSE;
}

关于Dl_info结构体和dladdr函数的内容,可以查看如下链接:
https://man7.org/linux/man-pages/man3/dladdr.3.html

2)读取目标文件的二进制内容并把二进制内容存放到struct stat结构体中:

struct stat fileStat;
if (stat(dl_info.dli_fname, &fileStat) < 0) {
    return FALSE;
}
if (!S_ISLNK(fileStat.st_mode) && !S_ISREG(fileStat.st_mode)) { // 如果不是符号链接文件或普通文件则返回错误
    return FALSE;
}
const char *filePath = dl_info.dli_fname;
FILE *f = fopen(filePath, "r");
if (f == NULL) {
    return FALSE;
}

3)使用mmap将文件的内容映射到到Elf64_Ehdr结构体中,然后解析该结构体,直至获取到build-id:

#if __x86_64__
#define ElfT(type) Elf64_##type
#else
#define ElfT(type) Elf32_##type
#endif
// mmap第一个参数表示要映射的内存的起始地址,一般为NULL,表示让系统自动选择
// 第二个参数为要映射文件的多大长度到内存
// 第三个参数表示映射区域的保护方式,此处为可读写
// 第四个参数表示映射区域的特性,此处 MAP_PRIVATE 表示对映射区域的写入 *** 作会产生一个映射文件的复制,对此映射区域作的任何修改都不会写回原来的文件内容。
// 第五个参数为文件描述符,使用fileno(f)将文件流转成文件描述符
// 第六个参数offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。
// ehdr为ELF文件的文件头结构体,ELF文件最开始的部分一定是ELF文件头,因此直接将mmap返回的首地址传给Elf64_Ehdr结构体即可获得ELF文件头的内容
ehdr = (ElfT(Ehdr) *)mmap(0, fileStat.st_size, PROT_READ|PROT_WRITE, MAP_PRIVATE, fileno(f), 0);
// ehdr->e_phoff成员的值为程序头表的文件偏移量(以字节为单位)。如果文件没有程序头表,则该成员为零。
// ehdr为ELF文件的首地址, 加上程序头表的文件偏移量, 即可得到程序头表的地址, 强转为 Elf64_Phdr结构体,即可得到程序头表的内容,
phdr = (ElfT(Phdr) *)(ehdr->e_phoff + (size_t)ehdr);
fclose(f);
// 可执行文件或共享对象文件的程序头表是一个结构数组,每个结构都描述了系统准备执行程序所需的段或其他信息。程序头表结构体 Elf64_Phdr 中的 p_type 成员为节区/段的类型,我们需要找到PT_NOTE类型的节,这里存放的是辅助信息(供应商或系统工程师需要用其他程序检查一致性、兼容性等的特殊信息来标记目标文件。SHT_NOTE类型的部分和PT_NOTE类型的程序头元素可用于此目的)的存放地址偏移
while (phdr->p_type != PT_NOTE) {
    ++phdr;
}
if (phdr->p_type != PT_NOTE) {
    return FALSE;
}
// 转到辅助信息的地址,强转为Elf64_Nhdr结构体,找到NT_GNU_BUILD_ID类型的成员
nhdr = (ElfT(Nhdr) *)(phdr->p_offset + (size_t)ehdr);
while (nhdr->n_type != NT_GNU_BUILD_ID)
{
    nhdr = (ElfT(Nhdr) *)((size_t)nhdr + sizeof(ElfT(Nhdr)) + nhdr->n_namesz + nhdr->n_descsz);
}
// NT_GNU_BUILD_ID类型的成员中存放的就是字节流格式的build-id
unsigned char *build_id = (unsigned char *)malloc(nhdr->n_descsz);
if (build_id == NULL) {
    return FALSE;
}
memset(build_id, 0, nhdr->n_descsz);
memcpy(build_id, (void *)((size_t)nhdr + sizeof(ElfT(Nhdr)) + nhdr->n_namesz), nhdr->n_descsz);

4)将字节流格式的build-id转为十六进制字符串:

char *buildIdStr = (char *)malloc(nhdr->n_descsz * 2); // 一个字节对应两位十六进制数字
for (int i = 0 ; i < nhdr->n_descsz ; ++i)
{
    if (snprintf(buildIdStr + 2 * i, 2 * nhdr->n_descsz, "%02x", build_id[i]) != 2) {
        free(build_id);
        return FALSE;
    }
}

参考链接:
mmap函数详解
ELF文件格式
ELF所有结构体注释
Elf64_Ehdr/Elf32_Ehdr结构体说明
Elf64_Phdr/Elf32_Phdr结构体说明

Elf32_Nhdr结构有点特殊,结构体后面紧跟着就是名称字段和描述符字段的内容:
typedef struct {
Elf64_Word n_namesz;
Elf64_Word n_descsz;
Elf64_Word n_type;
} Elf64_Nhdr;


typedef struct {
Elf64_Word n_namesz;
Elf64_Word n_descsz;
Elf64_Word n_type;
} Elf64_Nhdr;
··· ···
由上述可知,获取下一条ELF NOTE辅助信息的内容需要在当前地址加上结构体大小加上名称字段大小再加上描述符字段大小:
(size_t)nhdr + sizeof(ElfT(Nhdr)) + nhdr->n_namesz + nhdr->n_descsz

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5704502.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存