- [开发]为什么不建议交付静态链接的可执行文件给用户?
- 为什么Golang开发的软件单文件直接丢到各种Linux系统就能运行?
- [开发+运维] Linux从外到内剥开动态库,1个简单例子看懂Linux下的动态库开发原理
以上是近期发布的这3篇文章,围绕Linux下的可执行文件究竟是采用静态链接还是动态链接方式发布,从不同角度做了分析。看起来第1、2篇的观点是相反的。现在对这两个看似矛盾的说法加以解释,以期读者能对Linux的动态库、静态编译可执行文件的理解,和我一样在学习进步,形成总体符合实际的认知。
也得到了读者的很多反馈,现举例其一:
为什么要盯住gcc呢?因为Linux的软件生态中,C/C++开发的软件,特别是基础型软件、需要高性能的软件,往往历史原因都以这两种语言为主,在此不赘述。
总之,因为C/C++开发的软件占比很大,即使开发业务中不直接使用,也会在运行中依赖到C/C++的库或可执行文件的功能。搞懂动态库的特性,对于解决一些软件依赖问题、开发编译链接失败问题,都有帮助。所以开发者和运维仍然有必要增进这方面的理解。
1.动态链接适合插件化开发、插件化升级、希望打包发布的可执行文件尽量小的场景;而静态链接方式适合易部署、不想处理第三方动态库依赖问题的场景。
2.gcc/g++ 作为Linux下主要的编译器,支持动态链接、静态链接方式。如最基本的main.c 代码可通过gcc -o
main_dynamic_link main.c 和 gcc -static -o main_static main.c
分别得到两类可执行文件。这是大学生在学校初学Linux下的gcc C/C++编程的时候就了解的。
3.gcc 动态链接生成的可执行文件,因为代码必然使用到c/c++的标准库提供的函数,那么可执行文件必然要与libc.so库动态库链接(如下图)。
4.gcc编译得到的可执行文件,运行时会以进程方式在用户态、内核态的内存中布局。如果可执行文件是动态链接方式的,则运行时由 Linux内核负责载入ELF格式的可执行文件后,内核通过 ld-linux.so (64位系统下则为 ld-linux-x86-64.so ) 分析可执行文件依赖的其他动态库信息,由ld-linux.so 负责逐个载入其他动态库到该进程的虚拟内存的代码段位置中。这里就发挥了动态库和虚拟内存的优势:热门的动态库被很多其他进程依赖,那么这种可执行文件实际只占用物理内存的一块空间,无论被多少个进程依赖。
所以达到了提高内存利用率的效果,这对于需要运行大量软件的场景(如Linux桌面),收益还是可观的。可执行文件从被调起到执行完毕,我们可以用
strace 命令看到全过程,包括需要读取的其他库文件的过程。比如下面的可执行文件可以用strace
看到执行全流程(功能只调用printf函数打印字符串)。
root@localhost:~# file ./main
./main: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=82e7afc31da1cdbdd374658c2724dce983ccedab, for GNU/Linux 3.2.0, not stripped
root@localhost:~# ldd ./main
not a dynamic executable #说明当前是静态链接的
root@localhost:~# strace ./main
execve("./main", ["./main"], 0x7ffe6fd2a090 /* 25 vars */) = 0
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffe7ada0370) = -1 EINVAL (Invalid argument)
brk(NULL) = 0x1a12000
brk(0x1a12dc0) = 0x1a12dc0
arch_prctl(ARCH_SET_FS, 0x1a123c0) = 0
set_tid_address(0x1a12690) = 27080
set_robust_list(0x1a126a0, 24) = 0
rseq(0x1a12d60, 0x20, 0, 0x53053053) = 0
uname({sysname="Linux", nodename="localhost", ...}) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
readlink("/proc/self/exe", "/root/main", 4096) = 10
getrandom("xa1xe6x48xa4x1dx32xefx0e", 8, GRND_NONBLOCK) = 8
brk(0x1a33dc0) = 0x1a33dc0
brk(0x1a34000) = 0x1a34000
mprotect(0x4c1000, 16384, PROT_READ) = 0
newfstatat(1, "", {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0), ...}, AT_EMPTY_PATH) = 0
write(1, "111", 3111) = 3
exit_group(3) = ?
+++ exited with 3 +++
## 下面按动态链接生成可执行文件
root@localhost:~# gcc -o main main.c
## strace 显示可执行文件 执行时需要加载 `/lib/x86_64-linux-gnu/libc.so.6` 文件。
root@localhost:~# strace ./main
execve("./main", ["./main"], 0x7ffec2fb69c0 /* 25 vars */) = 0
brk(NULL) = 0x55fd3f1bc000
root@localhost:~#
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa3491dd000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=87359, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 87359, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa3491c7000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 >