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**。

解析ELF文件节区的内容,可以通过程序头表(programer header table)结构体Elf64_Phdr或节区头表(section header table)结构体Elf64_Shdr来进行解析,这两个结构的区别是:
programer header table只在可执行文件或动态链接文件中才会存在,是elf文件加载运行的时候才会用到,segment table是由0个或者多个section table组成的,两者的关系映射图如下:
在这里插入图片描述

最终方案

最后,结合我的需求,我选择了上面的方案三,在方案三的基础上做出了如下方案:
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结构体说明
ELF文件格式详解

Elf32_Nhdr结构体后面紧跟着就是名称字段和描述符字段的内容:
typedef struct {
Elf64_Word n_namesz;
Elf64_Word n_descsz;
Elf64_Word n_type;
} Elf64_Nhdr;
<n_name>
<n_desc>
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

<think>好的,我现在要帮助用户了解如何在Linux中查看一个文件是否为二进制文件。首先,我需要回顾用户之前的问题,他之前询问了文件权限字符串的含义,现在转向了如何判断文件类型是否为二进制。这可能意味着用户正在学习Linux文件管理的基础知识,或者在处理文件时遇到了相关问题。 用户的问题是如何在Linux中查看文件是否为二进制。我需要考虑几种常用的方法,并确保解释清晰,逐步指导。首先想到的命令是`file`,它可以直接显示文件类型。例如,`file 文件名`会输出文件类型信息,如果是二进制可执行文件,通常会显示“ELF”或“executable”。 接下来是`ls -l`命令,查看文件权限中的执行权限。如果有x权限,比如`-rwxr-xr-x`,可能表示是二进制可执行文件。但需要注意,脚本文件(如bash脚本)也可能有执行权限,所以这不能单独作为判断依据。 然后想到使用`strings`命令,这个命令可以提取文件中的可打印字符串。如果输出很多可读文本,可能是文本文件;如果很少或乱码,可能是二进制。但用户可能需要了解如何对比,比如`strings 文件名 | head`查看前几行。 另外,`head`或`cat`直接查看文件内容,如果出现乱码或非文本字符,可能是二进制。但直接查看可能对终端有影响,特别是如果文件包含控制字符,所以建议使用`cat -v`来显示非打印字符,或者用`hexdump`或`xxd`以十六进制形式查看。 可能还需要提到`grep`命令检查非文本字符,例如`grep -q "[^[:print:]]" 文件名`,如果匹配到非打印字符,可能是二进制。但这也可能有例外,比如文本文件包含特殊符号。 需要指出的是,没有绝对的方法,通常结合多种命令更可靠。比如先用`file`判断类型,再用`strings`辅助确认。同时提醒用户,文本文件如脚本可能有执行权限,而二进制文件可能包含部分文本,所以综合判断更准确。 用户可能的深层需求是区分可执行文件和文本文件,防止误编辑或执行。需要强调安全注意事项,如不要随意运行未知二进制文件。此外,用户可能希望自动化检测,比如在脚本中使用`file`命令的输出进行判断,提取关键词如“executable”或“ELF”。 总结步骤时,要分点说明每个方法的具体命令、输出示例以及如何解读结果。同时,比较不同方法的优缺点,帮助用户选择最合适的。例如,`file`命令最直接,而`strings`适合进一步分析内容。 最后,确保语言简洁,避免技术术语过多,必要时举例说明。例如,使用`file /bin/ls`会显示ELF可执行文件,而`file script.sh`显示脚本文本。这样用户能通过实例更好地理解不同命令的输出差异。</think>在Linux系统中,判断文件是否为二进制文件可以通过以下方法逐步实现: --- ### 方法1:使用 `file` 命令(最直接) **命令格式**: ```bash file 文件名 ``` **输出示例**: - 二进制文件: ```bash /bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., for GNU/Linux 3.2.0, stripped ``` - 关键词:`ELF`(可执行文件)、`executable`(可执行)、`binary`(二进制)。 - 文本文件: ```bash script.sh: Bourne-Again shell script, ASCII text executable ``` **原理**: `file` 命令通过分析文件内容的特征(魔数、编码等)判断文件类型。 --- ### 方法2:查看文件权限(辅助判断) **命令格式**: ```bash ls -l 文件名 ``` **输出示例**: ```bash -rwxr-xr-x 1 user group 123456 Jan 1 00:00 binary_file -rw-r--r-- 1 user group 100 Jan 1 00:00 text_file ``` - 若文件有 **执行权限**(如 `x`),可能是二进制可执行文件或脚本。 - **注意**:脚本文件(如 `.sh`)也可能有执行权限,需结合其他方法验证。 --- ### 方法3:使用 `strings` 命令(查看可打印字符) **命令格式**: ```bash strings 文件名 | head ``` **输出示例**: - 二进制文件: ```bash ELF /lib64/ld-linux-x86-64.so.2 ... ``` - 输出多为系统库路径、符号表等非人工可读内容。 - 文本文件: ```bash #!/bin/bash echo "Hello World" ``` **原理**: `strings` 提取文件中的可打印字符序列。二进制文件通常包含少量可读字符串,而文本文件多为连贯文字。 --- ### 方法4:用 `head`/`cat` 查看文件内容(谨慎使用) **命令格式**: ```bash head -n 5 文件名 # 查看前5行 cat -v 文件名 # 显示非打印字符(避免乱码) ``` **输出示例**: - 二进制文件: ```bash ^?ELF^B^A^A^@^@^@^@^@^@^@^@^@^C^@>^@^A^@^@^@ ``` - 出现乱码或控制字符(如 `^@` 表示空字节)。 - 文本文件: ```bash #!/bin/bash echo "This is a text file." ``` **注意**:直接查看二进制文件可能导致终端显示异常,建议使用 `cat -v` 或 `hexdump`。 --- ### 方法5:使用 `grep` 检测非文本字符 **命令格式**: ```bash grep -q "[^[:print:]]" 文件名 && echo "可能是二进制文件" ``` **原理**: 若文件包含**不可打印字符**(如空字节`\0`),则可能是二进制文件--- ### 方法6:十六进制查看(高级) **命令格式**: ```bash hexdump -C 文件名 | head ``` **输出示例(二进制文件)**: ```bash 00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 03 00 3e 00 01 00 00 00 a0 10 00 00 00 00 00 00 |..>.............| ``` - **ELF头**:二进制文件的典型标志(如 `7F 45 4C 46` 对应 ASCII 字符 `DEL` + `ELF`)。 --- ### 总结与注意事项 1. **优先使用 `file` 命令**:最可靠且安全。 2. **二进制文件与脚本的区别**: - 脚本(如 `.sh`)是文本文件,但可能有执行权限。 - 二进制文件(如 `.exe`、ELF)包含机器码,不可直接阅读。 3. **安全提示**: - 不要直接运行未知二进制文件(可能存在恶意代码)。 - 修改文件权限前务必确认文件来源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值