Skip to content

Commit 4697163

Browse files
feat: new link and compile/static_dynamic library tutorials
1 parent ca4fce2 commit 4697163

12 files changed

Lines changed: 2088 additions & 0 deletions

tutorial/深入理解CC++编译特性指南/深入理解CC++的编译与链接技术.md

Lines changed: 523 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
# 深入理解CC++的编译与链接技术(番外):动态库可以像可执行文件那样执行嘛?
2+
3+
我知道有朋友看到这个话题会下意识的发笑,会觉得笔者在胡言乱语。其实,笔者在最最开始的时候,也对这个事情一笑了之,觉得太荒唐。但是实际上,动态库是**可以像可执行文件那样执行的。**
4+
5+
会有人直接甩给我一个Segment Fault,告诉我你就是在胡言乱语。您可以自行切换到/lib目录下,找一个自己喜欢的库,比如说,笔者看重了libcurl库和libcrypt库,我们可以直接尝试执行它。
6+
7+
```
8+
[charliechen@Charliechen runaable_dynamic_library]$ /lib/libcurl.so
9+
Segmentation fault (core dumped) /lib/libcurl.so
10+
[charliechen@Charliechen runaable_dynamic_library]$ /lib/libcurl.so.4.8.0
11+
Segmentation fault (core dumped) /lib/libcurl.so.4.8.0
12+
[charliechen@Charliechen runaable_dynamic_library]$ /lib/libcrypt.so.2.0.0
13+
Segmentation fault (core dumped) /lib/libcrypt.so.2.0.0
14+
```
15+
16+
我们第一个想法是——为什么?为什么事情会变成这样?答案很简单,在后续的博客中,笔者会强调,一般而言,以.so结尾的,一般是动态库(或者说共享库,笔者已经说明了在今天的操作系统中,可以不再刻意的区分共享库和动态库了)
17+
18+
> [深入理解CC++的编译与链接技术2:动态库静态库导论-CSDN博客](https://blog.csdn.net/charlie114514191/article/details/154828385)
19+
20+
很显然,当我们直接输入文件的绝对地址的时候,操作系统的bash会尝试将它当作一个可独立运行的程序,然而,这个跟我们的动态库的定义:包含一组函数和数据的**动态共享组件**是不一致的。由于共享库没有设计像普通程序那样的标准主入口点($\text{main}$ 函数),直接运行时,执行流很可能跳转到无效的内存地址。操作系统检测到这种**非法内存访问**(试图访问程序无权访问的内存区域)时,就会触发**段错误**。我想很多人看到这里的时候,已经确信我这篇博客中指出:动态库是**可以像可执行文件那样执行的**这个论点就是错误的。
21+
22+
然而并不是,我们可以再次尝试一下执行C库:
23+
24+
```
25+
[charliechen@Charliechen runaable_dynamic_library]$ /lib/libc.so.6
26+
GNU C Library (GNU libc) stable release version 2.42.
27+
Copyright (C) 2025 Free Software Foundation, Inc.
28+
This is free software; see the source for copying conditions.
29+
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
30+
PARTICULAR PURPOSE.
31+
Compiled by GNU CC version 15.2.1 20250813.
32+
libc ABIs: UNIQUE IFUNC ABSOLUTE
33+
Minimum supported kernel: 4.4.0
34+
For bug reporting instructions, please see:
35+
<https://gitlab.archlinux.org/archlinux/packaging/packages/glibc/-/issues>.
36+
```
37+
38+
嗯?这个事情跟我们想的很不一样。这一次,C库不光没有SegmentFault,还甚至打印出来一段非常具备标识性的字符串并且优雅的退出了!很神秘对不对?没关系,笔者来带你一步一步探究到底发生了什么。
39+
40+
## 所以,到底怎么回事?
41+
42+
很简单, 我们这样开始——既然这个事情涉及到程序的运行开始,显然熟悉ELF文件格式的朋友就会指出——也许我们的玄机藏在ELF Header指向的地址中,几乎很容易猜到——肯定是libc的ELF Header指向的Entry Point跟咱们的一般的类似于libcurl这样的出于组件目的的库是**不一致的**。那么,查看ELF头信息的工具,就是大名鼎鼎的`readelf`工具。
43+
44+
我们需要强调一下ELF格式的一个基本知识——所有 ELF 文件(可执行文件和共享库)都有一个“入口点”,这是 CPU 开始执行指令的地方。或者说,告诉CPU的执行流(X86-64上是EIP或者RIP的值)一个确切的初始值。
45+
46+
```
47+
[charliechen@Charliechen runaable_dynamic_library]$ readelf -h /lib/libcurl.so
48+
ELF Header:
49+
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
50+
Class: ELF64
51+
Data: 2's complement, little endian
52+
Version: 1 (current)
53+
OS/ABI: UNIX - System V
54+
ABI Version: 0
55+
Type: DYN (Shared object file)
56+
Machine: Advanced Micro Devices X86-64
57+
Version: 0x1
58+
Entry point address: 0x0
59+
Start of program headers: 64 (bytes into file)
60+
Start of section headers: 945200 (bytes into file)
61+
Flags: 0x0
62+
Size of this header: 64 (bytes)
63+
Size of program headers: 56 (bytes)
64+
Number of program headers: 11
65+
Size of section headers: 64 (bytes)
66+
Number of section headers: 28
67+
Section header string table index: 27
68+
```
69+
70+
呦呵,这下不就真相大白了?如果我们尝试将`/lib/libcurl.so`视作一个可执行文件处理,那么这个时候,操作系统的加载器读取`/lib/libcurl.so`并且通过一般的检查后,将跳转地址设置成了`0x0`,啊哈,这不就访问空指针了嘛?
71+
72+
这就跟我们做这样的事情的性质完全是一样的!
73+
74+
```
75+
#include <stdio.h>
76+
77+
int main() {
78+
printf("Jumping to address 0x0...\n");
79+
void (*func)() = (void (*)())0x0;
80+
func();
81+
}
82+
```
83+
84+
编译并且执行它,得到的正好就是:
85+
86+
```
87+
[charliechen@Charliechen runaable_dynamic_library]$ gcc dump.c -o dump
88+
[charliechen@Charliechen runaable_dynamic_library]$ ./dump
89+
Jumping to address 0x0...
90+
Segmentation fault (core dumped) ./dump
91+
```
92+
93+
那么我们的libc库如何呢?
94+
95+
```
96+
[charliechen@Charliechen runaable_dynamic_library]$ readelf -h /lib/libc.so.6
97+
ELF Header:
98+
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
99+
Class: ELF64
100+
Data: 2's complement, little endian
101+
Version: 1 (current)
102+
OS/ABI: UNIX - GNU
103+
ABI Version: 0
104+
Type: DYN (Shared object file)
105+
Machine: Advanced Micro Devices X86-64
106+
Version: 0x1
107+
Entry point address: 0x27830
108+
Start of program headers: 64 (bytes into file)
109+
Start of section headers: 2145632 (bytes into file)
110+
Flags: 0x0
111+
Size of this header: 64 (bytes)
112+
Size of program headers: 56 (bytes)
113+
Number of program headers: 16
114+
Size of section headers: 64 (bytes)
115+
Number of section headers: 64
116+
Section header string table index: 63
117+
```
118+
119+
嗯?还真不一样,不要着急,只有一个`0x27830`,我们什么也不知道,下一步,就是请出我们的objdump大法看看细节:
120+
121+
> 会有朋友问我,为什么不是nm,嗯,对于动态库,nm暴露的是对外导出符号的地址,一般而言,你找不到EntryPoint对应的到底是什么。不过不用担心,我们还有一个招数,那就是objdump看反汇编。
122+
123+
```
124+
[charliechen@Charliechen runaable_dynamic_library]$ objdump -d /lib/libc.so.6 --start-address=0x27830 --stop-address=0x27860
125+
126+
/lib/libc.so.6: file format elf64-x86-64
127+
128+
129+
Disassembly of section .text:
130+
131+
0000000000027830 <gnu_get_libc_version@@GLIBC_2.2.5+0x10>:
132+
27830: f3 0f 1e fa endbr64
133+
27834: 55 push %rbp
134+
27835: bf 01 00 00 00 mov $0x1,%edi
135+
2783a: ba e3 01 00 00 mov $0x1e3,%edx
136+
2783f: 48 8d 35 5a d8 18 00 lea 0x18d85a(%rip),%rsi # 1b50a0 <__nptl_version@@GLIBC_PRIVATE+0x2b2d>
137+
27846: 48 89 e5 mov %rsp,%rbp
138+
27849: e8 d2 6c 0e 00 call 10e520 <__write@@GLIBC_2.2.5>
139+
2784e: 31 ff xor %edi,%edi
140+
27850: e8 7b d8 0b 00 call e50d0 <_exit@@GLIBC_2.2.5>
141+
27855: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
142+
2785c: 00 00 00
143+
2785f: 90 nop
144+
```
145+
146+
不必太着急,我们现在发动回忆大法,现在我们从0x27834开始,代码试图做这些事情:
147+
148+
> [x64.syscall.sh](https://x64.syscall.sh/),关于Syscall表,我放在这里了
149+
150+
- 将0x01放入edi,这里放置了第一个系统调用需要的参数
151+
152+
- 然后将第三个参数放到了edx中,拜托,这不就是字符串的长度嘛?十进制的 **483**
153+
154+
- 不要着急,我们稍后还需要放置的是rsi中的字符串地址,也就是第二个参数。注意到的是——指令是 `lea` (Load Effective Address),它将当前指令后的地址加上偏移量。所以不能直接去找0x18d85a,而是要加上当前指令的偏移量。
155+
156+
复习一下,objdump是如何算出来1b50a0的?首先,当前指令的基地址在:`0x2783f`,指令的长度本身是`48 8d 35 5a d8 18 00` 共 7 个字节。所以下一条指令在`0x2783f + 7 = 0x27846`。加上所给出的偏移地址,那就是——0x27846 + 0x18d85a = 0x1b50a0。OK,我们确信了objdump没有骗我们(大概率他当然不会!)
157+
158+
想要查看是不是真放的?
159+
160+
```
161+
[charliechen@Charliechen runaable_dynamic_library]$ hexdump -C -s 0x1b50a0 -n 483 /lib/libc.so.6
162+
001b50a0 47 4e 55 20 43 20 4c 69 62 72 61 72 79 20 28 47 |GNU C Library (G|
163+
001b50b0 4e 55 20 6c 69 62 63 29 20 73 74 61 62 6c 65 20 |NU libc) stable |
164+
001b50c0 72 65 6c 65 61 73 65 20 76 65 72 73 69 6f 6e 20 |release version |
165+
001b50d0 32 2e 34 32 2e 0a 43 6f 70 79 72 69 67 68 74 20 |2.42..Copyright |
166+
001b50e0 28 43 29 20 32 30 32 35 20 46 72 65 65 20 53 6f |(C) 2025 Free So|
167+
001b50f0 66 74 77 61 72 65 20 46 6f 75 6e 64 61 74 69 6f |ftware Foundatio|
168+
001b5100 6e 2c 20 49 6e 63 2e 0a 54 68 69 73 20 69 73 20 |n, Inc..This is |
169+
001b5110 66 72 65 65 20 73 6f 66 74 77 61 72 65 3b 20 73 |free software; s|
170+
001b5120 65 65 20 74 68 65 20 73 6f 75 72 63 65 20 66 6f |ee the source fo|
171+
001b5130 72 20 63 6f 70 79 69 6e 67 20 63 6f 6e 64 69 74 |r copying condit|
172+
001b5140 69 6f 6e 73 2e 0a 54 68 65 72 65 20 69 73 20 4e |ions..There is N|
173+
001b5150 4f 20 77 61 72 72 61 6e 74 79 3b 20 6e 6f 74 20 |O warranty; not |
174+
001b5160 65 76 65 6e 20 66 6f 72 20 4d 45 52 43 48 41 4e |even for MERCHAN|
175+
001b5170 54 41 42 49 4c 49 54 59 20 6f 72 20 46 49 54 4e |TABILITY or FITN|
176+
001b5180 45 53 53 20 46 4f 52 20 41 0a 50 41 52 54 49 43 |ESS FOR A.PARTIC|
177+
001b5190 55 4c 41 52 20 50 55 52 50 4f 53 45 2e 0a 43 6f |ULAR PURPOSE..Co|
178+
001b51a0 6d 70 69 6c 65 64 20 62 79 20 47 4e 55 20 43 43 |mpiled by GNU CC|
179+
001b51b0 20 76 65 72 73 69 6f 6e 20 31 35 2e 32 2e 31 20 | version 15.2.1 |
180+
001b51c0 32 30 32 35 30 38 31 33 2e 0a 6c 69 62 63 20 41 |20250813..libc A|
181+
001b51d0 42 49 73 3a 20 55 4e 49 51 55 45 20 49 46 55 4e |BIs: UNIQUE IFUN|
182+
001b51e0 43 20 41 42 53 4f 4c 55 54 45 0a 4d 69 6e 69 6d |C ABSOLUTE.Minim|
183+
001b51f0 75 6d 20 73 75 70 70 6f 72 74 65 64 20 6b 65 72 |um supported ker|
184+
001b5200 6e 65 6c 3a 20 34 2e 34 2e 30 0a 46 6f 72 20 62 |nel: 4.4.0.For b|
185+
001b5210 75 67 20 72 65 70 6f 72 74 69 6e 67 20 69 6e 73 |ug reporting ins|
186+
001b5220 74 72 75 63 74 69 6f 6e 73 2c 20 70 6c 65 61 73 |tructions, pleas|
187+
001b5230 65 20 73 65 65 3a 0a 3c 68 74 74 70 73 3a 2f 2f |e see:.<https://|
188+
001b5240 67 69 74 6c 61 62 2e 61 72 63 68 6c 69 6e 75 78 |gitlab.archlinux|
189+
001b5250 2e 6f 72 67 2f 61 72 63 68 6c 69 6e 75 78 2f 70 |.org/archlinux/p|
190+
001b5260 61 63 6b 61 67 69 6e 67 2f 70 61 63 6b 61 67 65 |ackaging/package|
191+
001b5270 73 2f 67 6c 69 62 63 2f 2d 2f 69 73 73 75 65 73 |s/glibc/-/issues|
192+
001b5280 3e 2e 0a |>..|
193+
001b5283
194+
```
195+
196+
足够了!后面的分析,显然就是将0作为exit的参数放置到edi中,并且优雅的退出了。
197+
198+
## 我们可以干这档事情嘛?
199+
200+
拜托!当然可以啊!现在笔者就陪你干一票!但是会有点难,因为我们现在不可能依赖libc库,因为动态库的初始化跟咱们的可执行程序有不一致的地方,比如说不会主动的初始化CRunTime,没办法主动链接C库(当然笔者之前做dynamic linker指定过,发现没有用,而且代码崩在了stack函数跳转上,有点无能为力了,搞半天没搞定)等等。
201+
202+
所以,现在我们可以搞一处了:
203+
204+
```
205+
#define NOT_API __attribute__((visibility("hidden")))
206+
207+
long NOT_API syscall_write(int fd, const char* buf, unsigned long len) {
208+
long ret;
209+
asm volatile(
210+
"syscall"
211+
: "=a"(ret)
212+
: "a"(1), "D"(fd), "S"(buf), "d"(len) // 1 is sys_write
213+
: "rcx", "r11", "memory");
214+
return ret;
215+
}
216+
217+
void NOT_API syscall_exit(int code) {
218+
asm volatile(
219+
"syscall"
220+
:
221+
: "a"(60), "D"(code) // 60 is sys_exit
222+
: "memory");
223+
}
224+
225+
unsigned long NOT_API ccstrlen(const char* s) {
226+
unsigned long i = 0;
227+
while (s[i])
228+
i++;
229+
return i;
230+
}
231+
232+
int add(int a, int b) {
233+
return a + b;
234+
}
235+
236+
void NOT_API _printf(const char* msg) {
237+
syscall_write(1, msg, ccstrlen(msg));
238+
}
239+
240+
int NOT_API direct_load_helper_main() {
241+
_printf("Hey! Welcome CCLibrary! "
242+
"These is a dynamic library helps math calculations\n");
243+
_printf("Current Version is 0.1.0\n");
244+
_printf("You can process add by using the library!\n");
245+
246+
// Must Call these to remind linux
247+
// to clear the stack
248+
syscall_exit(0);
249+
}
250+
251+
```
252+
253+
编译这段代码:
254+
255+
```bash
256+
gcc -shared -fPIC -o libcclib.so cclib.c -Wl,-e,direct_load_helper_main
257+
```
258+
259+
执行一下,就能得到结果了!
260+
261+
```bash
262+
[charliechen@Charliechen runaable_dynamic_library]$ ./libcclib.so
263+
Hey! Welcome CCLibrary! These is a dynamic library helps math calculations
264+
Current Version is 0.1.0
265+
You can process add by using the library!
266+
```
267+
268+
感兴趣的读者可以仿照笔者之前的分析重新走一遍流程。
269+
270+
那么问题来了,我们其他的可执行程序,可以像使用库那样,使用这个代码嘛?可以的。我们稍微把可见的add符号挪出来一个头文件:cclib.h
271+
272+
```
273+
#pragma once
274+
275+
int add(int a, int b);
276+
```
277+
278+
并且在main.c中像我们一般的库编程一样做这个事情:
279+
280+
```
281+
#include "cclib.h"
282+
#include <stdio.h>
283+
284+
int main() {
285+
int result = add(1, 2);
286+
printf("Result of 1 + 2 = %d\n", result);
287+
}
288+
289+
```
290+
291+
毫无压力!
292+
293+
```
294+
[charliechen@Charliechen runaable_dynamic_library]$ gcc main.c -o main ./libcclib.so
295+
[charliechen@Charliechen runaable_dynamic_library]$ ./main
296+
Result of 1 + 2 = 3
297+
```
298+

0 commit comments

Comments
 (0)