一、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.0gccconfigsol2-c.c
solaris_pragma_init (cpp_reader *pfile ATTRIBUTE_UNUSED)
该函数负责处理init指示。真正的生成代码的位置为gcc-4.1.0gccconfigsol2.c
void
solaris_output_init_fini (FILE *file, tree decl)
{
if (lookup_attribute ("init", DECL_ATTRIBUTES (decl)))
{
fprintf (file, " .pushsectiont".init"
");
ASM_OUTPUT_CALL (file, decl);
fprintf (file, " .popsection
");
}
if (lookup_attribute ("fini", DECL_ATTRIBUTES (decl)))
{
fprintf (file, " .pushsection ".fini"
");
ASM_OUTPUT_CALL (file, decl);
fprintf (file, " .popsection
");
}
}
/* Output a simple call for .init/.fini. */
#define ASM_OUTPUT_CALL(FILE, FN)
do
{
fprintf (FILE, " call ");
print_operand (FILE, XEXP (DECL_RTL (FN), 0), 'P');
fprintf (FILE, "
");
}
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
"
"call ininit
"
".popsection
");
void ininit()
{
printf("In init function
");
}
void inconstructor()
{
printf("In constructor attribute function
");
}
int main()
{
printf("In main
");
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.7libiostdio.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.7libiostdfiles.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