制作动态链接库

前言:在执行./pikafish/pikafish-avx2的时候发现glib库版本不对,如下:

1
2
3
4
dku@dku:~/桌面/workspaces$ ./pikafish/pikafish-avx2 
./pikafish/pikafish-avx2: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./pikafish/pikafish-avx2)
./pikafish/pikafish-avx2: /lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.29' not found (required by ./pikafish/pikafish-avx2)
./pikafish/pikafish-avx2: /lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.30' not found (required by ./pikafish/pikafish-avx2)

于是想查看一下我的系统中的glibc版本,已知有如下几种方式来查看:

  • ldd –version
  • /lib/x86_64-linux-gnu/libc.so.6

通过ldd --version查看没有什么好说的,ldd本身是一个sh脚本,我在其他的笔记中详细分析过这个脚本代码。但是仔细看/lib/x86_64-linux-gnu/libc.so.6,他是一个so文件啊,知识有限一直以为so文件不能执行,于是我执行了一下发现,竟然输出了如下内容:

1
2
3
4
5
6
7
8
9
10
dku@dku:~/桌面/workspaces$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.18) stable release version 2.31.
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 9.4.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

查了下一般情况下动态链接库(.so)确实是不能直接执行的,但是libc.so.6 能够执行是因为专门指定的“入口地址”。可以通过readelf命令来查看其入口点地址,可以看到是入口点地址: 0x241c0。因此虽然它是 .so,但在内核眼中,它既满足共享库的格式(ELF Shared Object),又包含了执行代码。这在 Linux 中被称为 PIE (Position Independent Executable) 技术的变体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
readelf -h /lib/x86_64-linux-gnu/libc.so.6
dku@dku:~/桌面$ readelf -h /lib/x86_64-linux-gnu/libc.so.6
ELF 头:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI 版本: 0
类型: DYN (共享目标文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x241c0
程序头起点: 64 (bytes into file)
Start of section headers: 2025240 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 14
Size of section headers: 64 (bytes)
Number of section headers: 68
Section header string table index: 67

为了弄明白其中道理,刚好来了解一下动态链接库的制作过程,这也是非常非常基础但是一直没有学习的内容了。

制作一个可以运行的动态链接库文件(.so)

先来制作一个普通的so文件

创建一个简单的 C 语言文件 mylib.c

1
2
3
4
5
6
7
8
9
10
11
// mylib.c
#include <stdio.h>

void hello_from_so() {
printf("你好!这是一个来自动态链接库的函数。\n");
}

int add(int a, int b) {
return a + b;
}

编译并生成 .so 文件

1
gcc -fPIC -shared -o libmylib.so mylib.c

其中:

  • -fPIC: 告诉编译器生成位置无关代码,这是动态库必需的。
  • -shared: 告诉链接器生成一个共享库文件,而不是可执行文件。

在程序中调用这个库

1
2
3
4
5
6
void hello_from_so(); // 声明外部函数

int main() {
hello_from_so();
return 0;
}

编译

1
2
# -L. 表示在当前目录找库,-lmylib 表示链接 libmylib.so
gcc main.c -L. -lmylib -o test_app

运行

由于 Linux 默认只在 /lib 或 /usr/lib 等标准路径找库,保险起见LD_LIBRARY_PATH 指定当前目录,但是在我的系统中没有指定也没有任何问题,结果如下。

1
LD_LIBRARY_PATH=. ./test
1
2
dku@dku:~/桌面$ ./test 
你好!这是一个来自动态链接库的函数。

制作一个可运行的so文件

本类以为制作一个可以运行的so文件只需要指定入口点就可以了,但是实际制作的时候发现总是不能运行,遇到各种各样的问题,我让ai帮我生成的C代码没有一个可以用的,最后还是纯汇编代码才成功运行。过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <unistd.h>
#include <stdio.h>

void hello_from_so() {
printf("你好!这是一个来自动态链接库的函数。\n");
}

int add(int a, int b) {
return a + b;
}

// 这是执行时调用的“入口”
void my_entry() {
char msg[] = "这是一个可以直接运行的自定义库!\n";
write(1, msg, sizeof(msg));
_exit(0); // 必须退出,否则会崩溃
}

编译

1
2
3
4
gcc -fPIC -shared -o libmylib_entry.so mylib_entry.c \
-Wl,-e,my_entry \
-Wl,--section-start=.interp=0x1000 \
-Wl,--dynamic-linker=/lib64/ld-linux-x86-64.so.2

理论上上述代码是没有问题的,并且通过以下命令也成功生成了libmylib_entry.so,但是一运行就出现段错误。这里将上述代码贴出来的主要目的是为了说明这样理论上是可以的,至于为什么不行可能与当前系统的各种环境比如栈对齐或C语言环境有关吧。

于是使用内联汇编重新生成了一份代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 用汇编编写入口,确保不依赖 C 库的启动流程
#include <unistd.h>

void my_entry() {
const char msg[] = "成功直接运行自定义 .so 文件!\n";

__asm__ __volatile__ (
"mov $1, %%rax;" // syscall: write
"mov $1, %%rdi;" // fd: stdout
"mov %0, %%rsi;" // buf: msg
"mov %1, %%rdx;" // count: len
"syscall;"
"mov $60, %%rax;" // syscall: exit
"xor %%rdi, %%rdi;"// status: 0
"syscall;"
:
: "r"(msg), "r"((long)sizeof(msg))
: "rax", "rdi", "rsi", "rdx"
);
}

重新编译(去掉 不稳定的 section-start)

1
2
3
gcc -fPIC -shared -o libmylib_entry.so mylib_entry.c \
-Wl,-e,my_entry \
-Wl,--dynamic-linker=/lib64/ld-linux-x86-64.so.2

结果运行又出现段错误,询问ai解释:直接运行 .so 经常崩溃是因为栈指针 (RSP) 未对齐或环境变量/辅助向量 (Auxiliary Vector) 未正确初始化,导致即使是简单的内联汇编也会在进入函数前因 push/mov 操作触发段错误

于是干脆生成一个纯汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <sys/syscall.h>

.section .text
.global my_entry

my_entry:
# 1. 直接发起 write 系统调用
mov $SYS_write, %rax # 系统调用号 (1)
mov $1, %rdi # 文件描述符 (stdout)
lea msg(%rip), %rsi # 字符串地址
mov $len, %rdx # 字符串长度
syscall

# 2. 直接发起 exit 系统调用
mov $SYS_exit, %rax # 系统调用号 (60)
xor %rdi, %rdi # 退出码 0
syscall

.section .rodata
msg:
.ascii "恭喜!自定义 .so 库直接运行成功!\n"
len = . - msg

# 关键:指定解释器路径到 .interp 段
.section .interp,"a"
.asciz "/lib64/ld-linux-x86-64.so.2"

直接编译汇编文件,并告诉链接器入口位置

1
gcc -fPIC -shared -o libmylib_entry.so mylib_entry.S -Wl,-e,my_entry
1
2
dku@dku:~/桌面$ ./libmylib_entry.so
恭喜!自定义 .so 库直接运行成功!

终于成功了!

查看两个so文件的区别

查看有入口的so:libmylib_entry.so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
dku@dku:~/桌面$ readelf -l libmylib_entry.so

Elf 文件类型为 DYN (共享目标文件)
Entry point 0x10f9
There are 10 program headers, starting at offset 64

程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x0000000000000230 0x0000000000000230 R 0x8
INTERP 0x0000000000002030 0x0000000000002030 0x0000000000002030
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000458 0x0000000000000458 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000131 0x0000000000000131 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000054 0x0000000000000054 R 0x1000
LOAD 0x0000000000002e80 0x0000000000003e80 0x0000000000003e80
0x00000000000001a0 0x00000000000001a8 RW 0x1000
DYNAMIC 0x0000000000002e90 0x0000000000003e90 0x0000000000003e90
0x0000000000000150 0x0000000000000150 RW 0x8
NOTE 0x0000000000000270 0x0000000000000270 0x0000000000000270
0x0000000000000024 0x0000000000000024 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RWE 0x10
GNU_RELRO 0x0000000000002e80 0x0000000000003e80 0x0000000000003e80
0x0000000000000180 0x0000000000000180 R 0x1

Section to Segment mapping:
段节...
00
01 .interp
02 .note.gnu.build-id .gnu.hash .dynsym .dynstr .rela.dyn
03 .init .plt .plt.got .text .fini
04 .rodata .interp .eh_frame
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.gnu.build-id
08
09 .init_array .fini_array .dynamic .got

查看普通so:libmylib_entry.so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
dku@dku:~/桌面t$ readelf -l libmylib.so

Elf 文件类型为 DYN (共享目标文件)
Entry point 0x1060
There are 11 program headers, starting at offset 64

程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000550 0x0000000000000550 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000155 0x0000000000000155 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000124 0x0000000000000124 R 0x1000
LOAD 0x0000000000002e10 0x0000000000003e10 0x0000000000003e10
0x0000000000000218 0x0000000000000220 RW 0x1000
DYNAMIC 0x0000000000002e20 0x0000000000003e20 0x0000000000003e20
0x00000000000001c0 0x00000000000001c0 RW 0x8
NOTE 0x00000000000002a8 0x00000000000002a8 0x00000000000002a8
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x00000000000002c8 0x00000000000002c8 0x00000000000002c8
0x0000000000000024 0x0000000000000024 R 0x4
GNU_PROPERTY 0x00000000000002a8 0x00000000000002a8 0x00000000000002a8
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002038 0x0000000000002038 0x0000000000002038
0x0000000000000034 0x0000000000000034 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002e10 0x0000000000003e10 0x0000000000003e10
0x00000000000001f0 0x00000000000001f0 R 0x1

Section to Segment mapping:
段节...
00 .note.gnu.property .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
01 .init .plt .plt.got .plt.sec .text .fini
02 .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.gnu.property
06 .note.gnu.build-id
07 .note.gnu.property
08 .eh_frame_hdr
09
10 .init_array .fini_array .dynamic .got
  • 关键差异:INTERP 段

libmylib_entry.so 中:
INTERP 0x... [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
而在普通的 libmylib.so 中,这个段是完全不存在的。
作用:当你尝试 ./ 运行一个文件时,Linux 内核会读取这个 INTERP 段。它告诉内核:“我不是一个独立的个体,请先加载这个 /lib64/ld-linux-x86-64.so.2 解释器来帮我跑起来。”
结论:没有这个段,内核不知道该如何处理动态库的符号寻址,所以会直接崩溃。

  • 入口点 (Entry point)

libmylib_entry.so 的入口是 0x10f9,这指向了汇编代码 my_entry
libmylib.so 虽然也有入口 0x1060,但那是 GCC 自动生成的默认初始化代码(通常是 _init)。因为没有 INTERP 指定解释器,内核在跳转到这个地址时,运行环境(寄存器、栈)都是错误的,导致一启动就触发 段错误

  • 内存权限 (Flags)

观察 GNU_STACK:可执行库显示的是 RWE (Read, Write, Execute)
普通库显示的是 RW,说明汇编版本获得了更宽松的执行权限。