从main()函数的执行发散开来

大多数程序员的第一行代码可能都是从输出“Hello,World!开始的吧。如果请你写一个c程序,在屏幕上打印“Hello,World!”,下面的代码对拥有扎实编程基本功的你而言肯定so easy:

#include <stdio.h>int main()
{printf("Hello, World!\n");return 0;
}

使用gcc编译运行:

(base) ~/Downloads/exmaples$ gcc main.c -o main
(base) ~/Downloads/exmaples$ ./main 
Hello, World!
(base) ~/Downloads/exmaples$ 

按照一般的编程套路,写了一个main()函数作为程序的入口函数。这个main()函数是梦(可能是噩梦,前方有难缠的八阿哥)开始的地方吗?

问题一 这个可执行文件大概依赖于哪些动态库?

假如你刚开始接触编程,只是浅浅的知道一个程序的运行需要依赖一些运行环境,但不太清楚究竟依赖哪些环境,这些环境又由哪些部门负责建立?为什么调用了一个printf函数,编译时没有链接任何库却可以编译通过并正常运行?

这些疑问,千锤百炼的编程大师们都想到了,并提供了一系列的工具来帮助你解开这些疑惑。

一般而言,动态链接的可执行程序需要依赖一些动态链接库。这些动态链接库或初始化程序运行的一些基础环境(比如堆栈),或辅助程序实现特定的功能(比如提供你需要的printf函数)。

对一个可执行程序而言,其运行所需要的库可以静态链接,也可以动态链接。

经常逛盒马的同学大概都可以看到一些事先为你烹饪好的美味佳肴,这些美味佳肴还有个响亮的名字--预制菜。制作美味佳肴需要的葱姜蒜、调料等已经和食材本身融为一体。即使是做菜小白,拿到预制菜放进微波炉加热一下也能无脑输出一道史诗级别的国民美食。

也有很多民间美食家喜欢自己动手,别人准备好的总归不一定百分百符合自己的口味。大厨们备菜一般不亲自出马,只负责烹饪的部分。拿到食材烹饪的过程中,需要什么配菜,加什么调料,都由大厨择机投放并严格控制用量,最后也能用脑输出一顿令人口口相传的家庭私房菜。

静态链接好比盒马的预制菜,烹饪所需的食材、配菜及调料作为一个整体被一次性打包。你中有我,我中有你,不能分离。

静态链接的程序在编译的时候被编译器将其依赖的模块和程序本身组装为一个整体,运行时被整体加载到内存中,如同做菜小白将预制菜放进微波炉加热一样。

动态链接好比民间美食家的烹饪。美味佳肴的烹饪手法、需要的配菜、调料等已经事先确定。烹饪过程中需要配菜、调料时再择机加入。食材本身、配菜、调料等是分离的。你是你,我是我。

动态链接的可执行程序所依赖的模块一般只有在真正需要的时候才由动态链接器加载至内存运行。

ldd命令可以用来查看一个程序依赖于哪些动态链接库。我们可以使用这个命令来一探究竟:

(base) ~/Downloads/exmaples$ ldd mainlinux-vdso.so.1 (0x00007fffb99f1000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa470f55000)/lib64/ld-linux-x86-64.so.2 (0x00007fa471168000)

libc.so.6

程序中调用printf向标准输出输出“Hello,World!”,printf是c语言标准库中的函数。

什么是c语言标准库呢?一个编程语言要建立自己的生态圈就要提高语言的易用性,其中一个很重要的部分,是把常用的功能封装成函数,以库的形式提供给用户使用。不然,用户无论使用什么功能都需要亲自实现一遍,不仅效率低下,而且在重复造轮子。

这时,权威组织(c语言标准委员会,由美国国家标准协会成立)就站出来了。它规定了哪些功能需要封装成库函数,并规定了这些库函数的具体形式。无论什么平台,什么操作系统,都需要支持这些函数形式。

c语言标准库可以看做c用户程序和不同操作系统平台之间的软件抽象层,它将不同的操作系统平台的API抽象成相同的库函数。这样,程序在各个平台和操作系统之间迁移就简单多了。

gcc编译的时候默认为程序动态链接了libc.so.6,即glibc。glibc是c语言标准库的一个超集,除了包含c语言标准库,还有几个辅助程序运行的运行库。这些运行库的功能包括初始化程序运行时环境(如堆的初始化)、调用用户入口函数等。

/lib64/ld-linux-x86-64.so.2

既然程序运行时需要动态加载glibc,那么肯定需要一个动态链接器将动态库加载到内存并与程序进行链接方能运行,/lib64/ld-linux-x86-64.so.2即是动态链接器。聪明的你又有疑问了,动态连接器本身又是被谁加载链接的呢?陷入了鸡生蛋蛋生鸡的死循环。

答案是动态链接器具有自举功能。

一个人不借助任何工具,自己把自己提起来,双脚腾空,这就是自举。

物理世界的自举(不借助外力提起自己)是不可能的(如果可以发生,请迅速广而告之!),但程序的自举是可以通过精心设计的层次结构实现的:

  1. 硬件/固件提供初始执行环境

  2. 引导程序通过固定入口点(如bios的0x7c00)获得控制权

  3. 逐级加载,比如引导程序→内核→用户空间

  4. 自引用构建:用简单版本构建复杂版本(如编译器自编译)

这种自举不是“凭空启动”,而是通过分层接力,让系统“拉起自己的鞋带”。自举在程序设计中比较常见,比如Go语言最初(Go 1.4及之前版本)的编译器是用c语言编写的。从Go 1.5版本开始,Go团队成功实现了编译器的自举。这意味着Go 1.5的编译器完全由Go语言本身完成。

实现动态链接器自举有什么挑战呢?

如果我们的程序静态链接了静态库a,调用了库a中的函数func_a,汇编伪代码可以是这样:jump func_a。这条指令可以理解为找到func_a在内存中的地址(这个地址存储了func_a实现的指令序列),跳到这个地址开始执行实现func_a的指令序列。

假设func_a的地址为1000,jump func_a被改写为jump 1000。

一个函数或变量的地址一开始不确定,某个阶段条件成熟后,才能准确确认它们的实际位置,这就叫做符号的重定位。

一个程序的编译大致可以分为编译、汇编、链接等过程。那么问题来了,编译器在编译用户的程序时,还没有把a链接进来,怎么处理调用func_a的代码呢,即jump后面的地址该怎么填?

对于静态链接,编译器的做法是任意填一个地址,链接时将用户程序和库a打包为一个整体。此时,func_a的地址就可以确定了,再由链接器将jump后面的地址改写为func_a实际的地址。

如果我们的程序动态链接了动态库b,调用了库b中的函数func_b。做法是否一样呢?

动态链接是被动态链接器在程序运行时动态加载到到内存中的,可以借鉴静态链接中更新函数地址的做法,在库b被加载到内存后由动态链接器最终确定func_b的实际位置。

如果这个动态链接库只服务一个进程,这种做法没问题。如果库b的函数func_b又调用了动态库c的函数func_c,且库b被多个进程同时使用,会有问题吗?

进程A将库b加载到自己的地址空间,将func_b中的jump func_c代码改写成jump 2000。进程B同样将库b加载到自己的地址空间。在进程B的地址空间发生func_b到func_c的调用时,拿到的代码是jump 2000。2000是进程A地址空间中的地址,该地址在进程B中可能无效,函数调用出错!

不仅如此,进程竟然可以修改代码段中的代码,这是不允许的。

动态链接库的代码段可以同时被多个进程共享,但数据段会被单独拷贝一份到各进程的地址空间。数据段是可以被进程修改的,如果它同时被多个进程修改,且这些进程之间无关联性,数据段中的数据就乱套啦!

一个动态链接库一般包含代码段和数据段。一段代码被编译成动态库后,代码段和数据段的相对位置就确定了。

利用动态链接库数据的独立性、代码段和数据段的相对位置不变性这两个特性,可以重新设计动态链接库中符号地址的重定位算法。

库b的数据段存有一张表格,这个表格的每一项存储的是需要重定位符号的实际地址,这个实际地址在动态链接器将库加载到进程的地址空间就可以确定下来了。

假如库b的数据段和jump func_c这条指令的偏移量为500,地址表格的位置在数据段的偏移量为50,func_c是地址表格中的第5项,那么jump func_c可以转变成jump 500 + 50 + 5 * 8(地址长度在64位机器上是8字节),找到这个地址后,取出该地址的存储内容就可以获取func_c在本进程的实际地址了。

jump 500 + 50 + 5 * 8这条指令就是地址无关指令,因为指令跳转的位置是相对位置。

动态链接库一般被多个进程同时使用,所以往往被编译成地址无关指令。

动态链接器自举代码的设计不可以使用任何全局变量,也不可以调用函数,因为没人帮它填充地址表格中符号的实际地址。

linux-vdso.so.1

用户空间的程序如果要操控比较底层的功能(比如通过文件系统访问磁盘)需要经过操作系统这道坎。

苹果的apple watch一直没有复制门禁卡的功能是因为没有nfc硬件吗?不是,是watch os没有开放这个功能给应用开发者而已!

我们去图书馆还书,先要去柜台找管理员,管理员拿到书后再将书放回到原来的位置。为什么管理员有这个特权而我们没有?

如果把这个特权交给普通读者呢?张三去还书,李四去还书,王五也去还书。张三素质比较高,知道严格按照要求将书放回原来的位置。李四和王五就差点意思,他们还书的位置随心情而定。久而久之,图书馆里书的放置位置就乱套了。

特权,只能掌握在专业可靠的人手中,为了安全,为了稳定。

为了安全和稳定人民群众是识大体的,但效率也确实低下。还书要麻烦图书管理员,图书馆的其它功能难道都需要请出专业的特权人员才能搞定吗?

有些涉及到系统安全稳定的功能不能妥协可以理解,但有些功能对系统的影响有限,采用一刀切的流程就没那个必要了。比如图书馆的某本书现在还剩多少本,最早归还的日期是多少等查询功能就可以直接向读者开放,读者在大厅的自动查询机上直接查询即可,没必要再去劳烦图书管理员饶一道圈圈。

传统的系统调用(申请运行内核空间的代码)需要从用户态切换到内核态。当发生系统调用时,程序需要进行上下文切换,保存用户态的寄存器、程序计数器等信息,加载内核态的寄存器、堆栈指针等。从内核态返回时,同样也会进行寄存器切换等操作。

linux-vdso.so.1是内核镜像的一部分,内核把一部分功能直接暴露给普通用户,普通用户可以直接在用户空间执行指令,不用陷入内核空间获取特权后才能运行这段代码,效率提升明显。

linux-vdso.so.1在文件系统中没有与之对应的具体文件,它是内核镜像的一部分,是内核虚拟出来的一个文件。为了看看这个文件究竟有些什么内容,需要采取一些手段。

首先,我们对main.c的内容做一些改造,让它在后台小睡300秒:

#include <unistd.h>int main()
{sleep(300);return 0;
}

编译,后台运行:

(base) ~/Downloads/exmaples$ gcc main.c -o main
(base) ~/Downloads/exmaples$ ./main &
[1] 555649

main进程的进程号为555649,看看它的虚拟地址分布:

(base) ~/Downloads/exmaples$ cat /proc/555649/maps
558e701ae000-558e701af000 r--p 00000000 103:02 3324120                   /home/solora/Downloads/exmaples/main
558e701af000-558e701b0000 r-xp 00001000 103:02 3324120                   /home/solora/Downloads/exmaples/main
558e701b0000-558e701b1000 r--p 00002000 103:02 3324120                   /home/solora/Downloads/exmaples/main
558e701b1000-558e701b2000 r--p 00002000 103:02 3324120                   /home/solora/Downloads/exmaples/main
558e701b2000-558e701b3000 rw-p 00003000 103:02 3324120                   /home/solora/Downloads/exmaples/main
7f740282a000-7f740282c000 rw-p 00000000 00:00 0 
7f740282c000-7f740284e000 r--p 00000000 103:02 14944449                  /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f740284e000-7f74029c6000 r-xp 00022000 103:02 14944449                  /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f74029c6000-7f7402a14000 r--p 0019a000 103:02 14944449                  /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7402a14000-7f7402a18000 r--p 001e7000 103:02 14944449                  /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7402a18000-7f7402a1a000 rw-p 001eb000 103:02 14944449                  /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7402a1a000-7f7402a1e000 rw-p 00000000 00:00 0 
7f7402a38000-7f7402a3a000 rw-p 00000000 00:00 0 
7f7402a3a000-7f7402a3b000 r--p 00000000 103:02 14943910                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7402a3b000-7f7402a5e000 r-xp 00001000 103:02 14943910                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7402a5e000-7f7402a66000 r--p 00024000 103:02 14943910                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7402a67000-7f7402a68000 r--p 0002c000 103:02 14943910                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7402a68000-7f7402a69000 rw-p 0002d000 103:02 14943910                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7402a69000-7f7402a6a000 rw-p 00000000 00:00 0 
7ffccfc88000-7ffccfcaa000 rw-p 00000000 00:00 0                          [stack]
7ffccfcd5000-7ffccfcd9000 r--p 00000000 00:00 0                          [vvar]
7ffccfcd9000-7ffccfcdb000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

vdso的地址范围为7ffccfcd9000-7ffccfcdb000,使用gdb命令将该地址范围的内容拷贝到一个名为vdso.dso的文件:

(base) ~/Downloads/exmaples$ sudo gdb -p 555649 -batch -ex "dump memory vdso.dso 0x7ffccfcd9000 0x7ffccfcdb000" -ex "detach" -ex "quit"
0x00007f74029091b4 in __GI___clock_nanosleep (clock_id=<optimized out>, clock_id@entry=0, flags=flags@entry=0, req=req@entry=0x7ffccfca7ea0, rem=rem@entry=0x7ffccfca7ea0) at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
78      ../sysdeps/unix/sysv/linux/clock_nanosleep.c: No such file or directory.
[Inferior 1 (process 555649) detached]

上面找不到的文件用于调试,不影响我们要讨论的内容,可以忽略。走到这里,就可以使用objdump查看linux-vdso.so.1的内容了:

(base) ~/Downloads/exmaples$ objdump -T vdso.dso vdso.dso:     file format elf64-x86-64DYNAMIC SYMBOL TABLE:
0000000000000a10  w   DF .text  0000000000000413  LINUX_2.6   clock_gettime
0000000000000690 g    DF .text  0000000000000348  LINUX_2.6   __vdso_gettimeofday
0000000000000e30  w   DF .text  0000000000000060  LINUX_2.6   clock_getres
0000000000000e30 g    DF .text  0000000000000060  LINUX_2.6   __vdso_clock_getres
0000000000000690  w   DF .text  0000000000000348  LINUX_2.6   gettimeofday
00000000000009e0 g    DF .text  0000000000000029  LINUX_2.6   __vdso_time
0000000000000ec0 g    DF .text  000000000000009c  LINUX_2.6   __vdso_sgx_enter_enclave
00000000000009e0  w   DF .text  0000000000000029  LINUX_2.6   time
0000000000000a10 g    DF .text  0000000000000413  LINUX_2.6   __vdso_clock_gettime
0000000000000000 g    DO *ABS*  0000000000000000  LINUX_2.6   LINUX_2.6
0000000000000e90 g    DF .text  0000000000000025  LINUX_2.6   __vdso_getcpu
0000000000000e90  w   DF .text  0000000000000025  LINUX_2.6   getcpu

文件包含了五个系统调用及对应的vdso实现,比如__vdso_gettimeofday对应gettimeofday,两个文件的地址完全一样。也就是说,内核开放了这五个系统调用的实现,这些实现可以直接在用户空间执行。

vdso 在技术上是"存在"于大多数进程中,但它不是传统意义上的库依赖,而是内核提供的优化机制。对于普通开发者来说,完全不需要关心它的存在。

问题二 不依赖printf打印Hello,World!行不行?

既然printf最终会调用write系统调用,直接使用write不行吗?

#include <unistd.h>int main()
{const char msg[] = "Hello, World!\n";size_t len = sizeof(msg) - 1;write(0, msg, len);return 0;
}

0表示标准输出,编译运行:

(base) ~/Downloads/exmaples$ gcc main.c -o main
(base) ~/Downloads/exmaples$ ./main 
Hello, World!

可见,直接调用write也是可行的,既然write能够实现同样的功能,c语言标准库为什么还需要引入printf这个函数呢?

引入printf函数能够满足复杂的数据格式化输出需求,提高代码的可读性和可维护性。在实际开发中,可以根据具体的应用场景和需求来选择使用printf或write。如果只是简单地输出数据,write系统调用可能更高效;但如果需要进行复杂的格式化输出,printf函数则更为合适。

能否既不使用printf也不使用write来实现相同的功能?

系统调用本质是通过cpu的陷阱机制(trap)主动触发从用户态到内核态的受控切换。可以直接在c程序中使用内联汇编指令调用系统调用:

void print()
{const char msg[] = "Hello, World!\n";size_t len = sizeof(msg) - 1;asm volatile ("movq $1, %%rax\n""movq $1, %%rdi\n""movq %0, %%rsi\n""movq %1, %%rdx\n""syscall": : "r"(msg), "r"(len): "rax", "rdi", "rsi", "rdx", "memory");
}

每个系统调用都有对应的编号,write的系统调用号为1,再通过寄存器传递write所需的参数,最后调用syscall触发trap就可以进入到内核态处理write系统调用的流程了。

用同样的方法可以实现一个类似exit()的函数:

void my_exit()
{asm volatile ("movq $60, %%rax\n""movq $42, %%rdi\n""syscall"::: "rax", "rdi", "rcx", "r11");
}

然后在main函数中调用这两个函数:

void main() {print();my_exit();
}

编译运行:

(base) ~/Downloads/exmaples$ gcc main.c -o main
(base) ~/Downloads/exmaples$ ./main 
Hello, World!
(base) ~/Downloads/exmaples$ echo $?
42

达到了同样的目的。

问题三 程序一定要从main()函数开始执行吗?

将main函数的名字改一下,换成别的名字,比如nomain:

#include <stdio.h>int nomain()
{printf("Hello, World!\n");return 0;
}

编译:

(base) ~/Downloads/exmaples$ gcc main.c -o main
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x24): undefined reference to `main'
collect2: error: ld returned 1 exit status

编译出错啦,错误提示包含了两个信息:

  1. 当运行这个程序的时候,首先调用的是_start,再由_start调用main
  2. _start是由Scrt1.o提供的函数

使用gcc -v输出编译时的更多信息:

(base) ~/Downloads/exmaples$ gcc -v main.c -o nomain/usr/lib/gcc/x86_64-linux-gnu/9/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/ccN6Ktn2.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o nomain /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/9 -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L. -L/usr/lib/gcc/x86_64-linux-gnu/9/../../.. /tmp/cctSwJdZ.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/9/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crtn.o
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':

gcc默认的编译行为链接了Scrt1.o这个目标文件(/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o)

使用nm查看Scrt1.o包含的符号:

(base) ~/Downloads/exmaples$ nm /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o
0000000000000000 D __data_start
0000000000000000 W data_startU _GLOBAL_OFFSET_TABLE_
0000000000000000 R _IO_stdin_usedU __libc_csu_finiU __libc_csu_initU __libc_start_mainU main
0000000000000000 T _start

也确实包含了_start函数。该入口点由内核在加载程序后跳转执行。_start负责初始化环境,设置栈,处理全局构造(对于 c++ 程序),然后调用main函数。

从git://sourceware.org/git/glibc.git下载glibc源码,Intel/AMD 64 位平台上_start的汇编实现在glibc/sysdeps/x86_64/start.S可以找到:

ENTRY (_start)|||call *__libc_start_main@GOTPCREL(%rip)|||
END (_start)

最终调用__libc_start_main,而传给该函数的参数为(main, argc, argv, init, fini, rtld_fini, stack_end)。至此,熟悉的main函数终于现身了。所有独立式程序(可执行文件)必须包含全局命名空间的main函数作为入口点,因此只能有一个main符号,不然会给_start造成困扰。

一个可执行程序的入口函数可以由链接脚本来控制,链接的默认脚本在哪里呢?ld是gcc编译时使用的链接器,可以利用--verbose参数打印链接的更多信息:

(base) ~/Downloads/glibc/sysdeps/x86_64$ ld --verbose
GNU ld (GNU Binutils for Ubuntu) 2.34Supported emulations:elf_x86_64elf32_x86_64elf_i386elf_iamcuelf_l1omelf_k1omi386pepi386pe
using internal linker script:
==================================================
/* Script for -z combreloc -z separate-code */
/* Copyright (C) 2014-2020 Free Software Foundation, Inc.Copying and distribution of this script, with or without modification,are permitted in any medium without royalty provided the copyrightnotice and this notice are preserved.  */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64","elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)------------------

输出的内容包含默认的链接脚本(using internal linker script:),其中ENTRY(_start)指定了程序的入口函数。

由此可见,如果采用gcc的默认编译行为,程序的入口必然是_start,_start必然会调用main函数,如果不提供main函数,编译必然报错。

既然链接脚本可以控制程序的入口函数,我们也可以依葫芦画瓢,自己写一个链接脚本控制程序的入口点:

ENTRY(nomain)SECTIONS
{. = 0x400000 + SIZEOF_HEADERS;
}

链接脚本的内容参考ld默认链接脚本的写法:

(base) ~/Downloads/exmaples$ ld --verbose | grep -A5 "SIZEOF_HEADERS"PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;.interp         : { *(.interp) }.note.gnu.build-id  : { *(.note.gnu.build-id) }.hash           : { *(.hash) }.gnu.hash       : { *(.gnu.hash) }.dynsym         : { *(.dynsym) }

0x400000是linux x86_64 架构的 ABI(应用程序二进制接口)规范 中定义的标准入口地址。可执行文件默认加载到虚拟地址空间的0x400000处。. = 0x400000 + SIZEOF_HEADERS表示将当前虚拟地址设置成0x400000 + SIZEOF_HEADERS,文件头与代码段物理连续,可单次内存映射完成,提高了装载时页映射的效率。

编译运行:

(base) ~/Downloads/exmaples$ gcc -c -fno-builtin -fno-stack-protector nomain.c
(base) ~/Downloads/exmaples$ ld -T nomain.lds -o nomain nomain.o
(base) ~/Downloads/exmaples$ ./nomain 
Hello, World!

现在,即使代码中没有main函数,程序也能正常运行输出结果。

问题四 怎样减小可执行文件的大小?

先看下目前可执行文件nomain的大小(1520字节):

(base) ~/Downloads/exmaples$ ls -l nomain
-rwxrwxr-x 1 solora solora 1520 6月  16 16:52 nomain

nomain中有哪些段呢?

(base) ~/Downloads/exmaples$ objdump -h nomainnomain:     file format elf64-x86-64Sections:
Idx Name          Size      VMA               LMA               File off  Algn0 .text         0000008a  0000000000400158  0000000000400158  00000158  2**0CONTENTS, ALLOC, LOAD, READONLY, CODE1 .eh_frame     00000078  00000000004001e8  00000000004001e8  000001e8  2**3CONTENTS, ALLOC, LOAD, READONLY, DATA2 .note.gnu.property 00000020  0000000000400260  0000000000400260  00000260  2**3CONTENTS, ALLOC, LOAD, READONLY, DATA3 .comment      0000002b  0000000000000000  0000000000000000  00000280  2**0CONTENTS, READONLY

通过链接脚本把除了代码段(.text)以外的三个段全部去掉看看程序运行是否正常:

nomain.lds:ENTRY(nomain)SECTIONS
{. = 0x400000 + SIZEOF_HEADERS;/DISCARD/ : { *(.comment) *(.eh_frame) *(.note.gnu.property) }
}(base) ~/Downloads/exmaples$ ld -T nomain.lds -o nomain nomain.o
(base) ~/Downloads/exmaples$ ./nomain 
Hello, World!
(base) ~/Downloads/exmaples$ objdump -h nomainnomain:     file format elf64-x86-64Sections:
Idx Name          Size      VMA               LMA               File off  Algn0 .text         0000008a  00000000004000e8  00000000004000e8  000000e8  2**0CONTENTS, ALLOC, LOAD, READONLY, CODE

现在nomain看起来只剩下.text段且能正常运行,那么现在的大小是多少呢?

(base) ~/Downloads/exmaples$ ls -l nomain
-rwxrwxr-x 1 solora solora 904 6月  16 17:09 nomain

从1520字节降到了904字节。

问题五 nomain还能进一步减小吗?

nomain真的只剩下.text段了吗?用另外一个工具readelf确认下:

(base) ~/Downloads/exmaples$ readelf -S nomain
There are 5 section headers, starting at offset 0x248:Section Headers:[Nr] Name              Type             Address           OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .text             PROGBITS         00000000004000e8  000000e8000000000000008a  0000000000000000  AX       0     0     1[ 2] .symtab           SYMTAB           0000000000000000  000001780000000000000090  0000000000000018           3     3     8[ 3] .strtab           STRTAB           0000000000000000  00000208000000000000001d  0000000000000000           0     0     1[ 4] .shstrtab         STRTAB           0000000000000000  000002250000000000000021  0000000000000000           0     0     1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),l (large), p (processor specific)

竟然还有三个顽固分子:.shstrtab、.symtab以及.strtab。它们分别是段名字符串表、符号表和字符串表。

什么是符号表呢?一个变量或函数总得有类型、名称、作用域等信息吧。符号名称给我们调试程序带来很大的便利,应该没人想对着一串数字来调试程序吧?这些名称又由字符串表负责存放。

在默认情况下,ld链接器在产生可执行文件时会产生这三个段。对于可执行文件来说,符号表和字符串表是可选的,但是段名字符串表用以保存段名,所以它是必不可少的。

可以使用strip命令去除nomain中的符号表:

(base) ~/Downloads/exmaples$ strip nomain
(base) ~/Downloads/exmaples$ ./nomain 
Hello, World!
(base) ~/Downloads/exmaples$ ls -l nomain
-rwxrwxr-x 1 solora solora 584 6月  16 17:25 nomain

nomain仍然能够正常运行输出结果,但此时nomain的大小从904字节降到了584字节。

现在nomain还剩下哪些段呢?

(base) ~/Downloads/exmaples$ readelf -S nomain
There are 3 section headers, starting at offset 0x188:Section Headers:[Nr] Name              Type             Address           OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .text             PROGBITS         00000000004000e8  000000e8000000000000008a  0000000000000000  AX       0     0     1[ 2] .shstrtab         STRTAB           0000000000000000  000001720000000000000011  0000000000000000           0     0     1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),l (large), p (processor specific)

没错,只剩下代码段(.text)和段名字符串表(.shstrtab)了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/84033.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/84033.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(16)java+ selenium->自动化测试-元素定位之By xpath下篇

1.简介 老规矩,我们还是接着前面两篇的Xpath 5.自动测试实战 以百度首页为例,将xpath的各种定位方法一一讲解和分享一下。 5.1大致步骤 1.访问百度首页。 2.通过xpath定位到元素,点击一下。 5.2模糊定位starts-with关键字 有一种特殊的情况:页面元素的属性值会被动态…

45-Oracle 索引的新建与重建

小伙们日常里有没有被业务和BOSS要求新建索引或是重建索引&#xff1f;他们都想着既快又稳&#xff0c;那么索引在在Oracle上如何实现、新建、重建。原则是什么&#xff1a; 1、新建索引&#xff0c;查询是否高频且慢&#xff0c;索引列是否高选择性&#xff0c;新增索引对写负…

使用 Rust Clippy 的详细方案

使用 Rust Clippy 的详细方案 Rust Clippy 是一个强大的静态分析工具&#xff0c;帮助开发者识别代码中的潜在问题并改善代码质量。以下是如何充分利用 Clippy 的方法&#xff1a; 安装 Clippy 确保 Rust 工具链已安装。通过以下命令安装 Clippy&#xff1a; rustup compon…

21.什么是JSBridge(1)

1.Native与H5交互的常用交互机制&#xff0c;主流选择是jsbridge 2.jsbridge是什么&#xff1f; JSBridge 是 Android 官方 WebView 提供的 addJavascriptInterface() 能力 项目方&#xff08;或三方库&#xff09;封装的桥梁通信协议。 底层机制由 Android 官方 WebView 提…

什么是Flink

Apache Flink&#xff1a;流批一体的大数据处理引擎 什么是Apache Flink&#xff1f; Apache Flink是一个开源的分布式流处理框架&#xff0c;最初由柏林工业大学开发&#xff0c;后成为Apache软件基金会的顶级项目。它能够以高吞吐、低延迟的方式处理无界数据流(流处理)和有…

区块链+智能合约如何解决上门按摩行业的信任问题?——App开发案例

你是不是觉得上门按摩市场已经人满为患&#xff1f;担心自己入局太晚或者缺乏行业经验&#xff1f;一组真实数据可能会让你改变看法&#xff1a;全国按摩服务需求正以月均8%的速度迅猛增长&#xff0c;但专业技师的供给量仅能跟上5%的增幅&#xff01;这意味着每个月都有相当于…

修改windows hosts文件的软件

修改hosts文件的软件推荐及使用教程 这个软件我用了10多年 推荐工具&#xff1a;Hosts Host软件不用安装绿色 如何使用 注意事项 如何没有安装.net 3.5 请根据提示安装就可以了 内容绑定了软件下载资源&#xff0c;在顶部有需要的自己取

Java web非Maven项目中引入EasyExcel踩坑记录

最近在帮朋友在老项目上做二次开发&#xff0c;有读取Excel的需求&#xff0c;习惯性的引入了EasyExcel&#xff0c;但是出现了很多问题&#xff0c;最主要就是jar包的问题,需要依赖的jar包版本问题 项目技术栈&#xff1a; tomcat9 Amazon Corretto JDK 8 (亚马逊的openJDK…

Flutter——数据库Drift开发详细教程(七)

目录 入门设置 漂移文件入门变量数组定义表支持的列类型漂移特有的功能 导入嵌套结果LIST子查询Dart 互操作SQL 中的 Dart 组件类型转换器现有的行类Dart 文档注释 结果类名称支持的语句 入门 Drift 提供了一个dart_api来定义表和编写 SQL 查询。尤其当您已经熟悉 SQL 时&#…

【排坑指南】MySQL初始化后,Nacos与微服务无法连接??

Date&#xff1a;2025/06/18 你好&#xff01; 今天&#xff0c;分享一个工作中遇到的一个 MySQL 问题。在这之前都不知道是 MySQL 的问题&#xff0c;特离谱&#xff01; 昨天和今天大多数时间都用来处理了这一个问题&#xff1a;《MySQL进行了数据库初始化之后&#xff0c…

springboot获取工程目录

在springboot中使用ApplicationHome获取工程所在目录的时候&#xff0c;开发环境和生产运行环境输出的目录是不同的&#xff0c;开发环境到target/classes目录&#xff0c;而生产运行则是需要的wzkj-server.jar所在目录 ApplicationHome home new ApplicationHome(CollectTas…

深入ZGC并发处理的原理

大型Java应用的核心痛点之一&#xff1a;当JVM进行垃圾回收时强制程序暂停&#xff08;STW&#xff09;的代价。在要求低延迟的应用场景——高频交易系统、实时在线服务或全球性大型平台——中&#xff0c;这种"时空静止"的成本可能极高。但JDK从16版本&#xff08;生…

配置DHCP服务(小白的“升级打怪”成长之路)

目录 项目前准备 一、DHCP服务器配置&#xff08;Rocky8&#xff09; 1&#xff0c;关闭防火墙、安全上下文 2、配置网卡文件 3、安装hdcp-server 4、配置dhcp服务 5、重启dhcp服务 二、配置路由器 1、添加两块网卡并更改网卡配置文件 2、配置路由功能 3、挂载本地镜…

云原生安全

云原生 | T Wiki 以下大部分内容参考了这篇文章 什么是云原生 云原生&#xff08;Cloud Native&#xff09; “云原生”可以从字面上拆解为“云”和“原生”两个部分来理解&#xff1a; “云”&#xff0c;是相对于“本地”而言的。传统应用部署在本地数据中心或物理服务器…

rapidocr v3.2.0发布

粗略更新日志 rapidocr v3.2.0 发布了。令我感到很开心的是&#xff1a;有 3 个小伙伴提了 PR&#xff0c;他们积极参与了进来。 更新要点如下&#xff1a; 采纳了小伙伴qianliyx 的建议&#xff0c;按照行返回单字坐标&#xff1a;同一行的单字坐标是在同一个 tuple 中的。…

Java 操作数类型冲突: varbinary 与 real 不兼容, Java中BigDecimal与SQL Server real类型冲突解决方案

要解决Java中BigDecimal类型与SQL Server中real类型冲突导致的varbinary与real不兼容错误&#xff0c;请按以下步骤操作&#xff1a; 错误原因分析 类型映射错误&#xff1a;JDBC驱动尝试将BigDecimal转换为varbinary&#xff08;二进制类型&#xff09;&#xff0c;而非目标字…

25.多子句查询

MySQL 中包含 GROUP BY、HAVING、ORDER BY、LIMIT 时的查询语法规则及应用&#xff0c;核心知识总结如下&#xff1a; 1.语法顺序规则 当 SELECT 语句同时包含 GROUP BY、HAVING、ORDER BY、LIMIT 时&#xff0c;执行顺序为&#xff1a; GROUP BY → HAVING → ORDER BY → L…

Vue3 × DataV:三步上手炫酷数据可视化组件库

DataV&#xff08;kjgl77/datav-vue3&#xff09;是专为“大屏可视化”场景打造的 Vue3 组件库&#xff0c;提供边框、装饰、等数十个开箱即用的视觉组件。本文聚焦 “在 Vue3 项目中如何正确使用 DataV”&#xff0c;从安装、全局注册到常见坑点&#xff0c;带你迅速玩转这款酷…

本地KMS服务器激活常用命令

OpenWRT内置了KMS激活的相关服务&#xff0c;配置后需要电脑本地切换到该KMS服务。相关命令如下&#xff1a; 基本功能与定义‌ slmgr是Windows内置的软件授权管理工具&#xff0c;全称为Software License Manager。其核心功能包括产品密钥安装/卸载、许可证信息查询、KMS服务器…

存货核算:个别计价法、先进先出法、加权平均法、移动加权平均法解读

存货作为企业资产的重要组成部分&#xff0c;贯穿于企业运营的各个环节&#xff0c;特别是制造业&#xff0c;企业的所有运营体系都是围绕存货来开展的。根据会计准则&#xff0c;存货是指企业在日常活动中持有以备出售的产成品或商品、处在生产过程中的在半成品&#xff0c;以…