一、so文件和exe文件
这两种文件其实具有很多相似自出,或者说so文件是介于obj文件和exe文件的一种中间过渡形式,它虽然不能直接运行(但是经过特殊编写的so文件内核是支持加载运行的,例如ld.so),但是具有了自己的一些更为高级的内容,例如一些初始化节,got表等内容,虽然孱弱,但是它具有了更加完善的生物形态。但是大家不要用进化论的观点来认为so文件比exe文件出现的早,事实上so是比较新的一个概念。我们看一下这些文件类型的标识类型说明
#define ET_NONE 0
#define ET_REL 1
#define ET_EXEC 2
#define ET_DYN 3
可以看到动态so文件的数值要比exe数值大。
这里说明这么多,就是so可以被通用的动态链接器识别和加载,由于so已经是一个exe的雏形,所以在加载一个so文件的时候可能除了完成动态链接之外还需要进行一些额外的操作,其中我们最为关心的就是个个so中定义的一些全局变量的初始化。这个概念对于通常的C文件生成的so没有直接意义,但是对于包含了全局变量的C++文件可能比较有用,也就是在这个so文件被加载之后、该文件中任何代码执行之前需要执行so中所有的初始化函数。
二、测试工程代码
//总共包含四个源文件,三个dep??.c生成各自的libdep??.so,main.c生成主程序。其中main.exe依赖dep11.so 和 dep12.so,而dep11.so进而依赖dep21.so,主要看这些文件的打开顺序以及初始化函数的执行顺序。
[tsecer@Harry soinit]$ ls
dep11.c dep12.c dep21.c main.c Makefile
[tsecer@Harry soinit]$ cat dep11.c
#include <stdio.h>
int __attribute__((constructor)) dep11(void)
{
return printf("In %s\n",__FUNCTION__);
}
[tsecer@Harry soinit]$ cat dep12.c
#include <stdio.h>
int __attribute__((constructor)) dep12(void)
{
return printf("In %s\n",__FUNCTION__);
}
[tsecer@Harry soinit]$ cat dep21.c
#include <stdio.h>
int __attribute__((constructor)) dep21(void)
{
return printf("In %s\n",__FUNCTION__);
}
[tsecer@Harry soinit]$ cat Makefile
main.exe:main.c libdep11.so libdep12.so
gcc -fPIC main.c -o $@ -L. -ldep11 -ldep12
LD_LIBRARY_PATH=. ./main.exe
libdep12.so libdep21.so:lib%.so:%.c
gcc -fPIC -shared -o $@ $<
libdep11.so:libdep21.so
gcc -fPIC -shared -o $@ -L. -ldep21 dep11.c
clean:
rm -f *.so *.exe *.o
//执行make之后输出的执行顺序,这个顺序我们稍后解释。
[tsecer@Harry soinit]$ make
gcc -fPIC -shared -o libdep21.so dep21.c
gcc -fPIC -shared -o libdep11.so -L. -ldep21 dep11.c
gcc -fPIC -shared -o libdep12.so dep12.c
gcc -fPIC main.c -o main.exe -L. -ldep11 -ldep12
/usr/bin/ld: warning: libdep21.so, needed by ./libdep11.so, not found (try using -rpath or -rpath-link)
LD_LIBRARY_PATH=. ./main.exe
In dep21
In dep12
In dep11
In dep00
三、动态库入口位置由来及意义
1、入口的形式
[tsecer@Harry soinit]$ readelf -a libdep11.so
ELF Header:
Magic: 7f 45 4c 46 01 01 01 03 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2‘s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - Linux
ABI Version: 0
Type: DYN (Shared object file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x370
Start of program headers: 52 (bytes into file)
Start of section headers: 1956 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 5
Size of section headers: 40 (bytes)
Number of section headers: 27
Section header string table index: 24
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-i NOTE 000000d4 0000d4 000024 00 A 0 0 4
[ 2] .gnu.hash GNU_HASH 000000f8 0000f8 00003c 04 A 3 0 4
[ 3] .dynsym DYNSYM 00000134 000134 0000b0 10 A 4 1 4
[ 4] .dynstr STRTAB 000001e4 0001e4 000090 00 A 0 0 1
[ 5] .gnu.version VERSYM 00000274 000274 000016 02 A 3 0 2
[ 6] .gnu.version_r VERNEED 0000028c 00028c 000030 00 A 4 1 4
[ 7] .rel.dyn REL 000002bc 0002bc 000028 08 A 3 0 4
[ 8] .rel.plt REL 000002e4 0002e4 000018 08 A 3 10 4
[ 9] .init PROGBITS 000002fc 0002fc 000030 00 AX 0 0 4
[10] .plt PROGBITS 0000032c 00032c 000040 04 AX 0 0 4
[11] .text PROGBITS 00000370 000370 000138 00 AX 0 0 16
[12] .fini PROGBITS 000004a8 0004a8 00001c 00 AX 0 0 4
这里一个比较有意思的现象就是so文件也有自己的入口地址,这个地址位于.text节的开始。但是明显的我们没有指定其实地址,对于通常的可执行程序,我们使用的是内置连接脚本,通过ld --verbose可以看到脚本内容。我们使用libdep11.so生成的命令添加 -v显示连接器使用的脚本:
[tsecer@Harry soinit]$ ld -shared -verbose
GNU ld version 2.19.51.0.14-34.fc12 20090722
Supported emulations:
elf_i386
i386linux
elf_x86_64
using internal linker script:
==================================================
/* Script for --shared -z combreloc: shared library, combine & sort relocs */
OUTPUT_FORMAT("elf32-i386", "elf32-i386",
"elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
SEARCH_DIR("/usr/i686-redhat-linux/lib"); SEARCH_DIR("/usr/local/lib"); SEARCH_DIR("/lib"); SEARCH_DIR("/usr/lib");
内置链接脚本同样使用的是_start符号,但是我们并没有定义这个符号,因为这个符号是在/usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crt1.o中定义的,而生成动态库的时候并没有链接这个文件,下面是通过gcc -v显示的动态链接命令
/usr/libexec/gcc/i686-redhat-linux/4.4.2/collect2 --eh-frame-hdr --build-id -m elf_i386 --hash-style=gnu -shared -o libdep11.so /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crti.o /usr/lib/gcc/i686-redhat-linux/4.4.2/crtbeginS.o -L. -L/usr/lib/gcc/i686-redhat-linux/4.4.2 -L/usr/lib/gcc/i686-redhat-linux/4.4.2 -L/usr/lib/gcc/i686-redhat-linux/4.4.2/../../.. -ldep21 /tmp/cc3PSy9f.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-redhat-linux/4.4.2/crtendS.o /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crtn.o
2、入口的由来
在链接器代码中,其中对于这个内容的处理比较特殊,链接器内部特地为so文件的生成做了兼容,所以此时不会出现链接错误:
binutils-2.21.1\ld\ldlang.c
static void
lang_end (void)
{
……
if ((link_info.relocatable && !link_info.gc_sections)
|| (link_info.shared && !link_info.executable))
warn = entry_from_cmdline; 对于动态链接文件来说,是否警告根据entry是否是命令行确定,由于我们例子中位于内置脚本中,所以不告警。
else
warn = TRUE;
……
{
bfd_vma val;
const char *send;
/* We couldn‘t find the entry symbol. Try parsing it as a
number. */
val = bfd_scan_vma (entry_symbol.name, &send, 0);
if (*send == ‘\0‘)
{
if (! bfd_set_start_address (link_info.output_bfd, val))
einfo (_("%P%F: can‘t set start address\n"));
}
else
{
asection *ts;
/* Can‘t find the entry symbol, and it‘s not a number. Use
the first address in the text section. */
ts = bfd_get_section_by_name (link_info.output_bfd, entry_section);如果找不到入口符号(即_start符号),则用entry_section地址代替。其定义为const char *entry_section = ".text";,所以就是text节作为共享库文件的入口,也就是我们看到和.text起始地址一致的原因。
if (ts != NULL)
{
if (warn)//这里的warn为false,所以没有告警。
einfo (_("%P: warning: cannot find entry symbol %s;"
" defaulting to %V\n"),
entry_symbol.name,
bfd_get_section_vma (link_info.output_bfd, ts));
if (!(bfd_set_start_address
(link_info.output_bfd,
bfd_get_section_vma (link_info.output_bfd, ts))))
einfo (_("%P%F: can‘t set start address\n"));
}
else
{
if (warn)
einfo (_("%P: warning: cannot find entry symbol %s;"
" not setting start address\n"),
entry_symbol.name);
}
}
}
}
3、入口的意义
对于大部分so文件没有意义,只有so真正独立运行时有意义,例如ld.so文件,这个让内核把任务的入口设置到该位置。
四、动态文件初始化顺序
1、init的由来
binutils-2.21.1\ld\ldmain.c
main (int argc, char **argv)
link_info.init_function = "_init";
link_info.fini_function = "_fini";
binutils-2.21.1\bfd\elflink.c
/* Add some entries to the .dynamic section. We fill in some of the
values later, in bfd_elf_final_link, but we must add the entries
now so that we know the final size of the .dynamic section. */
/* If there are initialization and/or finalization functions to
call then add the corresponding DT_INIT/DT_FINI entries. */
h = (info->init_function
? elf_link_hash_lookup (elf_hash_table (info),
info->init_function, FALSE,
FALSE, FALSE)
: NULL);
if (h != NULL
&& (h->ref_regular
|| h->def_regular))
{
if (!_bfd_elf_add_dynamic_entry (info, DT_INIT, 0))
return FALSE;
}
对于共享文件链接,通过之前命令可以看到,它的确链接了crti.o,其中也定义了_init函数
[tsecer@Harry soinit]$ nm /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crt1.o
00000000 R _IO_stdin_used
00000000 D __data_start
U __libc_csu_fini
U __libc_csu_init
U __libc_start_main
00000000 R _fp_hw
00000000 T _start
00000000 W data_start
U main
[tsecer@Harry soinit]$ nm /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crti.o
U _GLOBAL_OFFSET_TABLE_
w __gmon_start__
00000000 T _fini
00000000 T _init
最后定义了_init符号。
2、动态链接器打开共享库顺序
在dlopen--->>>dl_open_worker函数中,它执行大致流程为
_dl_map_object
_dl_map_object_deps
_dl_init (new, args->argc, args->argv, args->env)
也就是首先打开所有的直接依赖,这是一个递归过程,深度优先,其关键代码为
_dl_map_object_deps
for (d = l->l_ld; d->d_tag != DT_NULL; ++d)
if (__builtin_expect (d->d_tag, DT_NEEDED) == DT_NEEDED)
{
/* Map in the needed object. */
struct link_map *dep;
/* Recognize DSTs. */
name = expand_dst (l, strtab + d->d_un.d_val, 0);
/* Store the tag in the argument structure. */
args.name = name;
bool malloced;
int err = _dl_catch_error (&objname, &errstring, &malloced,
openaux, &args);
}
在一个so文件的所有依赖被加载完成之后,open函数将会一次调用所有的直接依赖的初始化入口函数,这个函数再负责遍历可能存在的、生成so时使用的各个obj文件中的init节中的函数指针数组。
_dl_init (struct link_map *main_map, int argc, char **argv, char **env)
{
i = main_map->l_searchlist.r_nlist;
while (i-- > 0)
call_init (main_map->l_initfini[i], argc, argv, env);
}
这里可以看到,此处函数执行的时候,它是执行的while(i--)指令,这意味着直接依赖的初始化函数的执行顺序是和在依赖中出现的顺序相反。所以我们看main.exe依次依赖了dep11.so和dep12.so,但是dep12.so的初始化函数的执行要早于dep11.so。
[tsecer@Harry soinit]$ readelf -d main.exe
Dynamic section at offset 0x650 contains 22 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libdep11.so]
0x00000001 (NEEDED) Shared library: [libdep12.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
3、动态链接器如何知道各个SO的初始化函数入口
[tsecer@Harry soinit]$ readelf -d main.exe
Dynamic section at offset 0x650 contains 22 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libdep11.so]
0x00000001 (NEEDED) Shared library: [libdep12.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x804836c
0x0000000d (FINI) 0x804857c
动过动态节中的DT_INIT和DT_FINI标签确定。
原文地址:https://www.cnblogs.com/tsecer/p/10486374.html