1. 程式人生 > >C/C++如何完成變量main函數之前初始化

C/C++如何完成變量main函數之前初始化

連接器 細節 fop 一個 是把 pfile 方法 jump 方便

一、gcc對main之前初始化的支持
對於變量的初始化,gcc提供了兩個相關功能,一個是
#pragma init(xxx)
,另一個是通過
__attribute__((constructor))
聲明的函數。
雖然說#pragma這個屬性只在soloris系統中有用,但是對於我們研究其實現原理還是很有幫助的。

這裏補充說一下,glibc和連接器還支持preinit_array和init_array兩種形式的初始化,但是和gcc關系不大,我們就不分析了,有興趣的同學可以通過
ld --verbose
看一下連接器內置連接腳本對該功能的支持,其中的preinit_array和init_array節的處理。
二、#pragma init的solaris實現


gcc-4.1.0\gcc\config\sol2-c.c
solaris_pragma_init (cpp_reader *pfile ATTRIBUTE_UNUSED)
該函數負責處理init指示。真正的生成代碼的位置為\gcc-4.1.0\gcc\config\sol2.c
void
solaris_output_init_fini (FILE *file, tree decl)
{
if (lookup_attribute ("init", DECL_ATTRIBUTES (decl)))
{
fprintf (file, "\t.pushsection\t\".init\"\n");
ASM_OUTPUT_CALL (file, decl);
fprintf (file, "\t.popsection\n");
}

if (lookup_attribute ("fini", DECL_ATTRIBUTES (decl)))
{
fprintf (file, "\t.pushsection\t\".fini\"\n");
ASM_OUTPUT_CALL (file, decl);
fprintf (file, "\t.popsection\n");
}
}
/* Output a simple call for .init/.fini. */
#define ASM_OUTPUT_CALL(FILE, FN) \
do \
{ \
fprintf (FILE, "\tcall\t"); \
print_operand (FILE, XEXP (DECL_RTL (FN), 0), ‘P‘); \
fprintf (FILE, "\n"); \
} \
while (0)
也就是在init節中放入了一條體系結構相關的指令 call symbol。
這裏引出了init節的一個重要特征,該節放的是指令,這裏的指令被順序執行(註意不是被call的)所以一個函數體不能放在該節
這個實現雖然只有在solaris系統下支持,但是它的實現方法應該是通用的,只要使用一些內聯匯編代碼,加上通過pushsection 和 popsection之前添加一個對該指令的call操作來完成。
三、__attribute__((constructor))實現
這個實現比較簡單,就是把一個需要被main之前調用的函數地址放入.ctor節即可,然後libgcc有專門的函數來遍歷這個函數指針,所以還是比較方便的。這說明一個基本事實,數據總是比代碼具有更強的跨平臺性。gcc的代碼就是把這個函數地址放入.ctor節,由另外的一個函數來遍歷這數組。
四、gcc相關實現文件
通過 gcc -v 查看一下編譯執行的命令
/usr/libexec/gcc/i686-redhat-linux/4.4.2/collect2 --eh-frame-hdr --build-id -m elf_i386 --hash-style=gnu -dynamic-linker /lib/ld-linux.so.2 /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crt1.o/usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crti.o /usr/lib/gcc/i686-redhat-linux/4.4.2/crtbegin.o -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/../../.. /tmp/cc32SDqb.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/crtend.o/usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crtn.o
可以看到在用戶真正使用的目標文件前後添加了一些特殊的目標文件,這些文件都是由系統提供。其中的crti和crtn就是我們通常所說的init的入口,而crtbegin和crtend就是對於構造函數的相關調用實現。具體內容可以通過objdump來看到該文件中的一些定義,但是這裏可以看到一個有趣的現象,就是構造函數的遍歷函數是放在crtend.o中的init節,而析構函數遍歷則是放在crtbeging.o的fini節,這意味著構造函數是所有的init代碼中最後被執行的,而析構函數則是所有fini函數中最早被執行的
[tsecer@Harry initfini]$ nm /usr/lib/gcc/i686-redhat-linux/4.4.2/crtbegin.o
w _Jv_RegisterClasses
00000000 d __CTOR_LIST__
U __DTOR_END__
00000000 d __DTOR_LIST__
00000000 d __JCR_LIST__
00000000 t __do_global_dtors_aux 該函數負責對所有析構函數遍歷
00000000 R __dso_handle
00000000 b completed.5934
00000004 b dtor_idx.5936
00000060 t frame_dummy
[tsecer@Harry initfini]$ nm /usr/lib/gcc/i686-redhat-linux/4.4.2/crtend.o
00000000 d __CTOR_END__
00000000 D __DTOR_END__
00000000 r __FRAME_END__
00000000 d __JCR_END__
00000000 t __do_global_ctors_aux 該函數負責對所有的構造函數進行遍歷
五、驗證init節、constructor屬性以及main的執行順序
[tsecer@Harry initfini]$ cat typical.c
#include <stdio.h>
void inconstructor() __attribute__((constructor));
__asm__ (
".pushsection .init\n"
"call ininit\n"
".popsection\n");
void ininit()
{
printf("In init function\n");
}
void inconstructor()
{
printf("In constructor attribute function \n");
}
int main()
{
printf("In main\n");
return 0;
}
[tsecer@Harry initfini]$ gcc typical.c
[tsecer@Harry initfini]$ ./a.out
In init function 自定義init節中函數最早被執行
In constructor attribute function constructo屬性函數次之
In main 最後是main函數執行
六、printf中使用的stdout文件何時可用
一般來說,在main函數裏這個文件是可用的,但是如果使用了構造函數,或者是更早的init節中為什麽可用呢?
glibc-2.7\libio\stdio.c
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;

glibc-2.7\libio\stdfiles.c
#ifdef _IO_MTSAFE_IO
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \
static struct _IO_wide_data _IO_wide_data_##FD \
= { ._wide_vtable = &_IO_wfile_jumps }; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
&_IO_file_jumps};
# else
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), \
&_IO_file_jumps};
# endif
#else
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static struct _IO_wide_data _IO_wide_data_##FD \
= { ._wide_vtable = &_IO_wfile_jumps }; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
&_IO_file_jumps};
# else
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), \
&_IO_file_jumps};
# endif
#endif

DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);
也就是說,這些結構不是通過fopen打開生成的,而是靜態手動構建的,所以在沒有執行一行用戶代碼的時候就已經完成初始化,由連接器生成可知行為文件時確定
七、鏈接器生成map文件的一個細節
查看生成的map文件,可以看到其中沒有objdump看見的frame_dummy,所以我們不知道這個函數是在哪裏定義的。至於為什麽,是因為連接器在生成map文件的時候不會打印出靜態變量在map文件中,而這個frame_dummy則剛好是位於crtbegin.o文件中的靜態符號。
[tsecer@Harry initfini]$ nm /usr/lib/gcc/i686-redhat-linux/4.4.2/crtbegin.o
w _Jv_RegisterClasses
00000000 d __CTOR_LIST__
U __DTOR_END__
00000000 d __DTOR_LIST__
00000000 d __JCR_LIST__
00000000 t __do_global_dtors_aux
00000000 R __dso_handle
00000000 b completed.5934
00000004 b dtor_idx.5936
00000060 t frame_dummy

C/C++如何完成變量main函數之前初始化