1. 程式人生 > >Linux動態鏈接(2)so初始化執行

Linux動態鏈接(2)so初始化執行

unix 入口 RKE final flink bin its r script ddr

一、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標簽確定。

Linux動態鏈接(2)so初始化執行