glibc/musl和ELF文件

简介

glibc和musl都是C语言的标准库,它们在功能上相似,都包括了很多常用的函数,如字符串处理、文件操作、进程控制等。但是它们在实现方式和设计哲学上存在一些不同。

glibc是C标准库的一个实现,包括头文件、函数库和其他应用程序。它为编译器(如g++和gcc)提供了基本的C和C++支持,并为应用程序提供了一组丰富的函数库。

musl libc也是一个C标准库的实现,但它强调自己是轻量级的,相比glibc更简单。在性能方面,musl的malloc系列函数和memcpy系列函数可能实现较慢,特别是在多线程环境下,这是因为musl的malloc实现需要在每次malloc时对全局变量加锁解锁,导致竞争现象。

musl是一个较新的C标准库,其设计哲学是简单、快速和可移植。它支持大多数C标准库函数,但可能缺少一些扩展功能。然而,由于它的轻量级和快速性能,它适用于一些嵌入式系统、移动设备和其他对性能有高要求的应用程序。

总的来说,glibc和musl在性能和实现方式上存在一些差异。

要使用musl,你需要先下载并安装musl的源文件,然后按照其文档中的指示进行编译和安装。安装完成后,你需要在你的程序中链接到musl库,以便能够使用其提供的的功能。总之,使用musl需要手动安装并配置正确的工作环境。

MIT和GPL

glibc是GPL授权,musl是MIT授权。

相同点:
1. 都是开源协议,授权使用、修改、分发和再发布软件。
2. 要求使用协议的用户保留原有许可证、著作权声明和免责声明等版权相关信息。
3. 未包含责任条款,表示对使用软件可能带来的损失,不负任何责任。
4. 允许在商业目的下使用软件。

不同点:
1. 授权方式不同。GPL协议要求使用该协议的软件必须以同样的GPL协议发布,而MIT协议则允许其他许可证的使用和发布。
2. 传染性不同。GPL协议是强制性的“传染性”协议,要求所有使用和修改过的代码都必须以同样的GPL协议发布。而MIT协议则不具备传染性,只要在代码中保留相应的许可证和著作权声明即可。
3. 对衍生作品的影响不同。GPL协议要求使用该协议的软件必须以同样的GPL协议发布,因此可能会对衍生作品产生限制或者对商业应用造成一定程度的不便。而MIT协议则更加宽松,适合商业软件的开发和应用。
4. 代码公开程度不同。MIT协议只要求保留相应许可证和著作权声明,并没有强制要求公开源代码。而GPL协议则要求在发布的软件中完整地包含源代码,并公开源代码。

总之,GPL协议更加强调开放、共享和社区合作,而MIT协议则更加注重商业应用和知识产权的保护,因此在不同的开源项目和应用场景下,开发者需要选择合适的开源协议。

1. GPL(GNU通用公共许可证)


GPL是一种强制性的许可证,要求任何使用或修改GPL许可的软件的衍生作品也必须使用GPL许可。这意味着如果你使用了GPL许可的代码,你的整个项目也必须使用GPL许可。

2. BSD(Berkeley软件分发许可证)

BSD许可是一种宽松的许可证,允许用户自由使用、修改和分发软件,而无需公开源代码。BSD许可允许用户将BSD许可的代码与其他许可证的代码混合使用。

3. MIT(麻省理工学院许可证)

MIT许可是一种宽松的许可证,允许用户自由使用、修改和分发软件,而无需公开源代码。与BSD许可类似,MIT许可也允许用户将MIT许可的代码与其他许可证的代码混合使用。

4. Mozilla(Mozilla公共许可证)

Mozilla许可是一种中等严格的许可证,要求用户在分发软件时必须公开源代码。此外,Mozilla许可还包含了一些专利授权条款,以保护软件的使用者免受专利侵权的影响。

5. Apache(Apache许可证)

Apache许可是一种宽松的许可证,允许用户自由使用、修改和分发软件,而无需公开源代码。与BSD和MIT许可类似,Apache许可也允许用户将Apache许可的代码与其他许可证的代码混合使用。

6. LGPL(GNU较宽松公共许可证)

LGPL是一种介于GPL和MIT/BSD之间的许可证。LGPL要求任何使用或修改LGPL许可的库的衍生作品也必须使用LGPL许可,但对于使用该库的应用程序没有强制要求。

移植

使用glibc编译的程序能直接调用musl库吗?

ld-linux-aarch64.so.1 是一个动态链接器(dynamic linker)的文件,用于ARM 64位架构上的Linux系统。

动态连接器是一个系统组件,负责在程序运行时加载共享库并解析它们的符号引用,以便程序能够正常执行。它负责将程序所需的共享库进行链接,处理库的加载、符号解析、重定位等操作。

程序在运行时会使用动态链接器来查找和加载共享库,并将程序的符号引用与共享库中的符号进行关联。当出现与动态链接器相关的错误时,如符号未定义或版本不匹配等,程序的正常执行可能会受到影响。

这个文件是GNU C库(glibc)提供的动态链接器的实现之一。我们RK的系统中使用了32位和64位两个glibc,所以ld程序也是两个,所以说程序使用哪个C库就要使用该C对提供的ld工具:

Linux和freeBSD

Freebsd是一个完整的系统架构,而Linux只是个内核和拥有各种发行版本。

FreeBSD和Linux都是开源的Unix-like操作系统,但是它们之间存在一些重要的区别,包括:

  1. 内核:FreeBSD使用自己的内核,而Linux使用Linux内核。

  2. 发行版本:FreeBSD有一个统一的发行版本,而Linux有许多不同的发行版本,如Ubuntu、Debian、Fedora等。

  3. 授权协议:FreeBSD使用BSD授权协议,而Linux使用GPL授权协议。

  4. 文件系统:FreeBSD默认使用UFS文件系统,而Linux使用ext文件系统。

  5. 软件包管理:FreeBSD使用Ports系统进行软件包管理,而Linux使用不同的包管理工具,如APT、YUM等。

  6. 硬件支持:FreeBSD对某些硬件的支持可能不如Linux全面。

  7. 社区:FreeBSD和Linux都拥有庞大的用户社区,但Linux的用户社区更加庞大和活跃。

总的来说,FreeBSD和Linux在很多方面都有相似之处,但也存在一些重要的区别,用户可以根据自己的需求和偏好选择合适的操作系统。

ld.so介绍

以ARM32开发板为例,在/lib下有一个名为ld-linux-armhf.so.3的可执行程序(在ARM64开发板上是/lib/ld-linux-aarch64.so.1),这个程序负责加载可执行程序以及依赖的动态库:

You have invoked `ld.so', the helper program for shared library executables.
This program usually lives in the file `/lib/ld.so', and special directives
in executable files using ELF shared libraries tell the system's program
loader to load the helper program from this file.  This helper program loads
the shared libraries needed by the program executable, prepares the program
to run, and runs it.  You may invoke this helper program directly from the
command line to load and run an ELF executable file; this is like executing
that file itself, but always uses this helper program from the file you
specified, instead of the helper program file specified in the executable
file you run.  This is mostly of use for maintainers to test new versions
of this helper program; chances are you did not intend to run this program.

  --list                list all dependencies and how they are resolved
  --verify              verify that given object really is a dynamically linked
                        object we can handle
  --inhibit-cache       Do not use /etc/ld.so.cache
  --library-path PATH   use given PATH instead of content of the environment
                        variable LD_LIBRARY_PATH
  --inhibit-rpath LIST  ignore RUNPATH and RPATH information in object names
                        in LIST
  --audit LIST          use objects named in LIST as auditors

然后使用--list参数就可以查看依赖的库:

# /lib/ld-linux-armhf.so.3 --list /usr/bin/stressapptest 
        linux-vdso.so.1 (0xbeeef000)
        librt.so.1 => /lib/librt.so.1 (0xb6ece000)
        libpthread.so.0 => /lib/libpthread.so.0 (0xb6eaa000)
        libstdc++.so.6 => /lib/libstdc++.so.6 (0xb6d9f000)
        libm.so.6 => /lib/libm.so.6 (0xb6d23000)
        libgcc_s.so.1 => /lib/libgcc_s.so.1 (0xb6cfa000)
        libc.so.6 => /lib/libc.so.6 (0xb6c0d000)
        /lib/ld-linux-armhf.so.3 (0xb6ee4000)

此外,也可以使用readelf来查看库的依赖关系:

readelf -a <可执行程序> | grep NEEDED

interpreter不同和ABI差异

不同的C语言库使用不同的加载器(interpreter),这是阻碍绝大多数elf文件在各个系统之间兼容的主要因素,甚至同为Linux,glibc和musl也互不兼容。

Linux ABI不兼容错误通常发生在尝试运行为不同Linux版本或者不同架构的系统编译的程序时。ABI(Application Binary Interface)定义了程序和其库之间的互动接口,如果接口不匹配,就可能出现不兼容的情况。

其次就是interpreter不同,但是基本所有的共享库和二进制都是elf格式,使用readelf -l可以查看程序头表(Program header table),interpreter的位置就在这里.

在Linux下运行FreeBSD的程序就会提示:zsh: 没有那个文件或目录: sbin/zfs

实际上是interpreter找不到的通用提示.(当然也很具有迷惑性,因为文件明明就在那里).

通过readelf的-h选项可以看到ABI的版本信息:

ELF文件

Linux ELF是计算机程序。ELF = Executable and Linkable Format,可执行连接格式,是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的。扩展名为elf。

一个ELF头在文件的开始,保存了路线图,描述了该文件的组织情况。sections保存着object文件的信息。从连接角度看:包括指令、数据、符号表、重定位信息等。特别section的描述会出现在以后的第一部分。

文件类型

在早期的UNIX中,可执行文件格式为a.out格式,由于其格式简单,随着共享库概念的出现被COFF格式取代,后来Linux和Windows基于COFF格式,分别制定了ELF和PE格式,我们日常使用的".exe"文件".lib",".dll"文件就属于PE文件的一种;Linux平台下的可执行文件,中间目标文件".o"以及静态库".a"和动态链接库".so"文件属于ELF文件,本节主要讲解中间目标文件(Relocatable File in ELF)这一ELF类型的文件结构,因为这是编译器将源代码经过预编译,编译,汇编后得到的第一层ELF文件,目标文件经过链接后才能成为真正在Linux下运行的可执行文件,这一类型会在后续blog中讲解。

我们以一段具有代表性的C文件来作为测试源码,如下,该程序中包含了各种类型的变量以及函数,这些变量或者函数符号经过汇编生成中间目标文件后将被保存在不同的ELF段里。

 1 int printf( const char* format, ... ); // 外部函数声明
 2 
 3 int global_init_var = 84; // 全局已赋值变量
 4 int global_uninit_var;    // 全局未初始化变量
 5 
 6 void func1( int i ) {
 7     printf("%d\n", i);
 8 }
 9 
10 int main(void) {
11     static int static_var = 85; // 局部静态已赋值变量
12     static int static_var2;     // 局部静态未初始化变量
13     int a = 1; // 局部已赋值变量
14     int b;     // 局部未初始化变量
15 
16     func1( static_var + static_var2 + a + b);
17     return a;
18 }

 我们使用gcc编译出汇编文件,中间目标文件,以及最终可执行文件,最后使用"file"工具查看文件类型,使用"tree"查看生成的文件大小 : 

 1 $ ls
 2 SimpleSection.c
 3 $ gcc -S SimpleSection.c && gcc -c SimpleSection.c -o SimpleSection.o && gcc SimpleSection.c -o SimpleSection
 4 $ tree -sp
 5 .
 6 ├── [-rwxrwxr-x        8512]  SimpleSection
 7 ├── [-rw-rw-r--         395]  SimpleSection.c
 8 ├── [-rw-rw-r--        1936]  SimpleSection.o
 9 └── [-rw-rw-r--        1336]  SimpleSection.s
10 
11 $ file *
12 SimpleSection:   ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=da891be2d625e27300a0a9682a57fb6cf6563d82, not stripped
13 SimpleSection.c: C source, ASCII text
14 SimpleSection.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
15 SimpleSection.s: assembler source, ASCII text

可以看到,文件"SimpleSection"和"SimpleSection.o"均为ELF-64文件,但是类型不一样,"SimpleSection.o"文件大小为1936个字节。后文使用sublime text以16进制查看ELF目标文件。

Linux Standard Base (LSB):

LSB 是 Linux 标准化领域中事实上的标准,LSB目前是 FSG(Free Standards Group)中最为活跃的一个工作组,其使命是开发一系列标准来增强 Linux 发行版的兼容性,使各种软件可以很好地在兼容 LSB 标准的系统上运行,从而可以帮助软件供应商更好地在 Linux 系统上开发产品,或将已有的产品移植到 Linux 系统上。

LSB 以 POSIX 和 SUS 标准为基础,并对其他领域(例如图形)中源代码的一些标准进行了扩充,还增加了对二进制可执行文件格式规范的定义,从而试图确保 Linux 上应用程序源码和二进制文件的兼容性。

目标文件ELF结构

ELF文件总体结构可以用图1表示,图左为"SimpleSection.o"文件的前一部分以十六进制表示的内容,图中间一层层的字段(定义:每种字段存储不同类型的内容)就是ELF结构的内容层次了,在目标文件的开头为一个长度为64(0x40)字节的ELF头,只要分析ELF表头内存储的信息,可以得出段表"Section Header table"(在图的最顶层的那个段)在整个目标文件中的偏移,而段表是一个元素为"Elf64_Shdr"结构体类型的数组,它的元素的数量正好是图中间那些字段的数量,也就是它的每个元素存储了中间一些字段如".text",".symtab"等字段的信息,这些信息包括字段名,大小等等属性。所以概括的来说,通过读取ELF头,可以得到段表,然后通过读取段表中各个字段元素,就可以得出各个字段的信息了。那么读到这里,ELF结构轮廓已然清晰,接下来就是分析ELF文件各个字段的具体用途,以及某些字段是具体如何关联才使得链接器能够完全理解这个文件。

.text字段:用于保存程序中的代码片段;

.data字段:用于保存已经初始化的全局变量和局部变量;

.bss: 用于保存未初始化的全局变量和局部变量;BSS是英文Block Started by Symbol的简称。

.rodata: 顾名思义,保存只读变量

.comment: 保存编译器版本信息;

.systab: 符号表,各个目标文件链接的接口;

.strtab: 字符串表,保存符号的名字,因为各个字符串大小不一,所以统一把字符串放到这个段里,后续其他段通过某个符号在字符串表中的偏移可以取到符号;

.rela.text: 因为程序声明使用了未在程序内部定义的函数或者变量,所以需要等到链接时(定义在别的目标文件或者库里)对这个符号的地址进行重新定位,不然会引用到错误的地址。

.shstrtab: 和strtab类似,不过保存的是段名,也就是说里面保存的字符串是所有段的名字。

Section Header Table: 段表,保存了所有段的信息,本身通过ELF头找到,可以解析出所有段的位置。

在你还没掌握肉身解码ELF文件之前,你可能需要一些工具才能得出目标文件中的什么字段,每个字段有多少字节等信息,我们可以借助“objdump”和“readelf”来查看目标文件内的细节:

  • 使用readelf工具,我们可以很容易得到 "ELF Header前16个字节(可用sublime查看文件实际内容验证)" 为 "7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00" ,前4个字节被称为为Elf文件的魔数。前四个字节为所有ELF固定的格式,第5个字节为0x02代表的是64位Elf,如果你的机器是32位的话,那么这个字节就是0x01,第6个字节为字节序,0x01为little endian,0x02为big endian,第7个字节为Elf文件的主版本号,后面9个字节一般为0。
  • 另外,这个shell工具还给我们输出了各个字段的信息,和图1中各个字段一一对应,我们可以看到各个字段在文件中的偏移和占用的字节,如字段".text"的offset为0x40(这里同时验证了Elf文件头为0x40个字节大小,和readelf返回的"Size of this header: 64 (bytes)"契合),占用了0x57个字节。其他字段的位置和大小以此类推。
  • 除此之外,我们可以看到“Start of section headers: 1104(bytes into file)”这一信息,这表示段表的位置在文件的1104(0x450)个字节的偏移的地方。而我们的ELF一共有13个段,因为段表中每个元素表示的是各个段的属性而不是内容,所以段表中的每个元素的大小应该是一样的,实施上,每个元素都是一个ELF64_shdr结构体类型的变量,该结构体和其他如表示ELF头的结构体"ELF64_EHDR"都可以在文件“/usr/include/elf.h”中找到,每个结构体有64个字节(也可以从readelf的输出得出:Size of section headers: 64(bytes)),那么段表将在1104+64*13=1936 Bytes结束,正好是我们用“ls -l”得出来的目标文件大小,这表明在我这个平台下,段表位于目标文件的最后。

段表元素类型:Elf64_Shdr 

 1 typedef struct
 2 {
 3     Elf64_Word    sh_name;        /* Section name (string tbl index) */
 4     Elf64_Word    sh_type;        /* Section type */
 5     Elf64_Xword   sh_flags;       /* Section flags */
 6     Elf64_Addr    sh_addr;        /* Section virtual addr at execution */
 7     Elf64_Off sh_offset;      /* Section file offset */
 8     Elf64_Xword   sh_size;        /* Section size in bytes */
 9     Elf64_Word    sh_link;        /* Link to another section */
10     Elf64_Word    sh_info;        /* Additional section information */
11     Elf64_Xword   sh_addralign;       /* Section alignment */
12     Elf64_Xword   sh_entsize;     /* Entry size if section holds table */
13 } Elf64_Shdr;

 Elf文件头类型:Elf64_Ehdr

 1 typedef struct
 2 {
 3     unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
 4     Elf64_Half    e_type;         /* Object file type */
 5     Elf64_Half    e_machine;      /* Architecture */
 6     Elf64_Word    e_version;      /* Object file version */
 7     Elf64_Addr    e_entry;        /* Entry point virtual address */
 8     Elf64_Off e_phoff;        /* Program header table file offset */
 9     Elf64_Off e_shoff;        /* Section header table file offset */
10     Elf64_Word    e_flags;        /* Processor-specific flags */
11     Elf64_Half    e_ehsize;       /* ELF header size in bytes */
12     Elf64_Half    e_phentsize;        /* Program header table entry size */
13     Elf64_Half    e_phnum;        /* Program header table entry count */
14     Elf64_Half    e_shentsize;        /* Section header table entry size */
15     Elf64_Half    e_shnum;        /* Section header table entry count */
16     Elf64_Half    e_shstrndx;     /* Section header string table index */
17 } Elf64_Ehdr;

上文中我们说到可以通过ELF头得到段表在文件中的位置从而可以找到段表。保存这个偏移的信息就保存在结构体“Elf64_Ehdr”中的成员"e_shoff"中,我们回过头来看Elf表头的内容,使用sublime text打开目标文件,观察文件头前64个字节,也就是Elf文件头的大小。

1 7f45 4c46 0201 0100 0000 0000 0000 0000
2 0100 3e00 0100 0000 0000 0000 0000 0000
3 0000 0000 0000 0000 5004 0000 0000 0000
4 0000 0000 4000 0000 0000 4000 0d00 0c00

可以看到,在第41个字节开始的8个字节(成员e_shoff为Elf64_Off类型,是uint64_t的typedef)为 "5004000000000000",由于是little endian,所以转化成十进制就是0x0450=1104,正好和readelf输出的偏移一样。

  • 我们再来观察由Elf头得到的段表,也就是文件从第1105个字节开始的段,前128个字节(包含了NULL和.text字段的信息)如下:
1 0000 0000 0000 0000 0000 0000 0000 0000
2 0000 0000 0000 0000 0000 0000 0000 0000
3 0000 0000 0000 0000 0000 0000 0000 0000
4 0000 0000 0000 0000 0000 0000 0000 0000
5 
6 2000 0000 0100 0000 0600 0000 0000 0000
7 0000 0000 0000 0000 4000 0000 0000 0000
8 5700 0000 0000 0000 0000 0000 0000 0000
9 0100 0000 0000 0000 0000 0000 0000 0000

因为第一个字段为NULL,所以64个字节全为0,接下来的一个字段就是.text字段,对比一下 "Elf64_Shdr"结构体的定义,刚好是64个字节,其中成员 "sh_offset" 表示该段在文件中的偏移,该结构体的第25-32这8个字节为0x40,是"sh_offset"的值,而该结构体的第23-40这8个字节为0x57,是 "sh_size" 的值,正好是偏置0x40,大小0x57,和readelf工具的输出一致。

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李小白20200202

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值