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