写点什么

程序一定要从 main 函数开始运行吗?

发布于: 2020 年 06 月 15 日
程序一定要从main函数开始运行吗?

程序一定要从main函数开始运行吗?本文涉及静态链接相关知识。



对于静态链接先提出两个问题:



Q:每个目标文件都有好多个段,目标文件在被链接成可执行文件时,输入目标文件中的各个段如何被合并到输出文件?



A:合并相似的段,将所有的.text段合并到输出文件的.text段,将所有的.data段合并到输出文件的.data段。



Q:链接器如何为他们分配在输出文件中的空间和地址?



A:这里涉及到程序链接的两个步骤:



  1. 空间与地址分配:扫描所有的输入目标文件,获得它们每个段的长度属性和位置,收集输入目标文件中的符号表中的所有符号定义和符号引用,统一放到一个全局符号表中,合并所有的段,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。

  2. 符号解析与重定位:使用第一步收集到的所有信息,读取输入文件中段的数据及重定位信息,进行符号解析和重定位,调整代码中的地址,将每个段中需要重定位的指令和数据进行“修补”,使他们都指向正确的位置。



tips:外部符号指的是目标文件需要引用的符号,但是定义在其它目标文件中,链接前外部符号地址都是000000之类,链接后的可执行文件就可以看见这些外部符号都是有地址的。链接就是把相似的段放在一起,先找到段的偏移地址,再找出符号在段中的偏移,这样可以确定符号在整个可执行程序中的地址。



对于那些需要重定位的符号,都会放在重定位表里,也叫重定位段,即.rel.data、.rel.text等,如果.text段有被重定位的地方,就有.rel.text段,如果.data段有被重定位的地方,就有.rel.data段。可以使用objdump查看目标文件的重定位表。



源代码:



int main() {
printf("程序喵\n");
return 0;
}
gcc -c test



objdump -r test.o
test.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000007 R_X86_64_PC32 .rodata-0x0000000000000004
000000000000000c R_X86_64_PLT32 puts-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text

使用nm也可以查看需要重定位的符号:



nm -u test.o
U _GLOBAL_OFFSET_TABLE_
U puts

对于UND类型,这种未定义的符号都是因为该目标文件中有关于他们的重定位项,在链接器扫描完所有的输入目标文件后,所有这种未定义的符号都应该能在全局符号表中找到,否则报符号未定义错误。



注意:我们代码里明明用的是printf,为什么它却引用了puts的符号呢,因为编译器默认情况下会把只用一个字符串参数的printf替换成puts, 可以节省格式解析的时间,使用-fno-builtin会关闭这个内置函数优化选项,如下:



~/test$ gcc -c -fno-builtin testlink.cc -o test.o
~/test$ nm test.o
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
U printf

tips:现在的程序和库通常来讲都很大,一个目标文件可能包含成百上千个函数或变量,当需要用到某个目标文件的任意一个函数或变量时,就需要把它整个目标文件都链接进来,也就是说那些没有用到的函数也会被链接进去,这会导致链接输出文件变的很大,造成空间浪费。



有一个编译选项叫函数级别链接,可以使得某个函数或变量单独保存在一个段里面,都链接器需要用到某个函数时,就将它合并到输出文件中,对于没用到的函数则将他们抛弃,减少空间浪费,但这会减慢编译和链接过程,GCC编译器的编译选项是:



-ffunction-sections
-fdata-sections

可能很多人都会以为程序都是由main函数开始执行和结束的,但其实不是,在main函数调用之前,为了保证程序可以顺利进行,要先初始化进程执行环境,如堆分配初始化、线程子系统等,C++的全局对象构造函数也是这一时期被执行的,全局析构函数是main之后执行的。



Linux一般程序的入口是__start函数,有两个段:



  • .init段:进程的初始化代码,一个程序开始运行时,在main函数调用之前,会先运行.init段中的代码。

  • .fini段:进程终止代码,当main函数正常退出后,glibc会安排执行该段代码。



如何指定程序入口



在ld链接过程中使用-e参数可以指定程序入口,由于一段简短的printf函数其实都依赖了好多个链接库,我们也不太方便使用链接脚本将目标文件与所有这些依赖库进行链接,所以使用下面这段内嵌汇编的程序来打印一段字符串,这段程序不依赖任何链接库就可以打印出字符串内容,读者如果不懂其中的含义也不用担心,只需要了解下面介绍的链接知识就好。



代码如下:



const char* str = "hello";
void print() {
asm("movl $13,%%edx \n\t"
"movl str,%%ecx \n\t"
"movl $0,%%ebx \n\t"
"movl $4,%%eax \n\t"
"int $0x80 \n\t"
:
:"r"(str):"edx", "ecx", "ebx");
}
void exit() {
asm("movl $42,%ebx \n\t"
"movl $1,%eax \n\t"
"int $0x80 \n\t");
}
void nomain() {
print();
exit();
}

使用如下命令生成目标文件:



gcc -c -fno-builtin test.cc

看下输出的test.o的符号:



~/test$ nm -a test.o
0000000000000000 b .bss
0000000000000000 n .comment
0000000000000000 d .data
0000000000000000 d .data.rel.local
0000000000000000 r .eh_frame
0000000000000000 n .note.GNU-stack
0000000000000000 r .rodata
0000000000000000 t .text
0000000000000026 T _Z4exitv
0000000000000000 T _Z5printv
0000000000000039 T _Z6nomainv
0000000000000000 D str
0000000000000000 a test.cc

这里由于我的源文件是.cc结尾,所以是以c++方式编译的,所以符号变成了上面的形式,如果变成了test.c,符号如下:



~/test$ gcc -c -fno-builtin test.c -o test.o
~/test$ nm -a test.o
0000000000000000 b .bss
0000000000000000 n .comment
0000000000000000 d .data
0000000000000000 d .data.rel.local
0000000000000000 r .eh_frame
0000000000000000 n .note.GNU-stack
0000000000000000 r .rodata
0000000000000000 t .text
0000000000000026 T exit
0000000000000039 T nomain
0000000000000000 T print
0000000000000000 D str
0000000000000000 a test.c

再使用-e指定入口函数符号:



~/test$ ld -static -e nomain -o test test.o
~/test$ ./test
hello
如何使用自定义链接脚本实现自定义段的功能



在ld链接过程中使用-T参数可以指定链接脚本,通过ld -verbose可以查看默认的链接脚本,原文太长,这里简单截取了一部分:



$ ld -verbose
GNU ld (GNU Binutils for Ubuntu) 2.30
Supported emulations:
elf_x86_64
elf32_x86_64
elf_i386
elf_iamcu
i386linux
elf_l1om
elf_k1om
i386pep
i386pe
using internal linker script:
==================================================
/* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2018 Free Software Foundation, Inc.
Copying and distribution of this script, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
"elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
SECTIONS
{
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
.init :
{
KEEP (*(SORT_NONE(.init)))
}
.plt : { *(.plt) *(.iplt) }
.plt.got : { *(.plt.got) }
.plt.sec : { *(.plt.sec) }
.text :
{
*(.text.unlikely .text.*_unlikely .text.unlikely.*)
*(.text.exit .text.exit.*)
*(.text.startup .text.startup.*)
*(.text.hot .text.hot.*)
*(.text .stub .text.* .gnu.linkonce.t.*)
/* .gnu.warning sections are handled specially by elf32.em. */
*(.gnu.warning)
}
.fini :
{
KEEP (*(SORT_NONE(.fini)))
}
.rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
/DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}

这里自定义一个简单的链接脚本test.lds



ENTRY(nomain)
SECTIONS
{
. = 0x8048000 + SIZEOF_HEADERS;
tinytext : { *(.text) *(.data) *(.rodata) }
/DISCARD/ : { *(.comment) }
}

再使用-T指定链接脚本:



~/test$ ld -static -T test.lds -e nomain -o test test.o
~/test$ ./test
hello

上面的tinytext一行是指将.text段、.data段、.rodata段的内容都合并到tinytext段中,使用readelf查看段的信息。



~/test$ readelf -S test
~/test$ There are 6 section headers, starting at offset 0x482a0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .eh_frame PROGBITS 00000000080480b0 000480b0
0000000000000078 0000000000000000 A 0 0 8
[ 2] tinytext PROGBITS 0000000008048128 00048128
0000000000000066 0000000000000000 WAX 0 0 8
[ 3] .shstrtab STRTAB 0000000000000000 0004826e
000000000000002e 0000000000000000 0 0 1
[ 4] .symtab SYMTAB 0000000000000000 00048190
00000000000000c0 0000000000000018 5 4 8
[ 5] .strtab STRTAB 0000000000000000 00048250
000000000000001e 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

工具小贴士



关于静态链接库:



ar rcs libxxx.a xx1.o xx2.o 打包静态链接库
ar -t libc.a 查看静态链接库里都有什么目标文件
ar -x libc.a 会解压所有的目标文件到当前目录
gcc --verbose 可以查看整个编译链接步骤

关于objdump:



objdump -i 查看本机目标架构
objdump -f 显示文件头信息
objdump -d 反汇编程序
objdump -t 显示符号表入口,每个目标文件都有什么符号
objdump -r 显示文件的重定位入口,重定位表
objdump -x 显示所有可用的头信息,等于-a -f -h -r -t
objdump -H 帮助

关于分析ELF文件格式:



readelf -h 列出文件头
readelf -S 列出每个段
readelf -r 列出重定位表
readelf -d 列出动态段

关于查看目标文件符号信息:



nm -a 显示所有的符号
nm -D 显示动态符号
nm -u 仅显示没有定义的外部符号
nm -defined-only 仅显示定义的符号

关于符号的说明:



如果符号类型是小写的,表明符号是局部符号,大写表示符号是全局符号。



  • A:该符号的值是绝对的,在以后的链接过程中,不允许进行改变。这样的符号值,常常出现在中断向量表中,例如用符号来表示各个中断向量函数在中断向量表中的位置。

  • B:该符号的值出现在.bss段中,未初始化的全局和静态变量。

  • C:该符号的值在COMMON段中,里面的都是弱符号。

  • D:该符号位于数据段中。

  • I:该符号对另一个符号的间接引用

  • N:debug符号

  • R:该符号位于只读数据区

  • T:该符号位于代码段

  • U:该符号在当前文件未定义,定义在别的文件中

  • ?:该符号类型没有定义



参考资料



https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/



《程序员的自我修养》

更多文章,请关注我的V X 公 主 号:程序喵大人,欢迎交流。



发布于: 2020 年 06 月 15 日阅读数: 54
用户头像

安身立命 (公众号:程序喵大人) 2018.05.26 加入

公众号:程序喵大人

评论

发布
暂无评论
程序一定要从main函数开始运行吗?