本篇文章主要介绍了"Android C语言_init函数和constructor属性及.init/.init_array节探索",主要涉及到Android C语言_init函数和constructor属性及.init/.init_array节探索方面的内容,对于Android C语言_init函数和constructor属性及.init/.init_array节探索感兴趣的同学可以参考一下。
了解C语言的程序猿都知道有两种方法可以让一部分代码在so或可执行文件被加载的时候先于其它任何函数执行,一种是定义一个void _init(void)函数,另一种是在函数后面声明constructor属性。那么这两种方式在执行的时候有什么区别吗?先后顺序呢?了解ELF文件格式的人又会问它们在文件中的位置又有什么差别呢?这篇文章就来解答这些问题。
首先你需要了解一下ELF文件格式了,这里就不啰嗦了,不了解的人可以搜一下看看。
下面是一个例子,在你的Android工程中的C/C++代码中加入下面几行:
........ #ifdef __cplusplus extern "C" { #endif void _init(void){mlog_info("_init enter");} #ifdef __cplusplus } #endif void __attribute__((constructor)) myConstructor(void){mlog_info("myConstructor enter\n");} ........
我这边编译出来是libcheckcert.so文件,放到手机上去运行的结果是:
........ 12-13 11:04:46.603: I/BRIAN(12203): _init enter 12-13 11:04:46.603: I/BRIAN(12203): myConstructor enter ........
_init函数是最先运行的,为什么会这样呢?了解ELF文件的人都知道有.init和.init_array这两个节,它们是ELF文件在加载的时候用来做初始化的,那么它们和_init函数及constructor属性有什么关系呢?下面我们需要借助readelf和IDA pro来查看,首先readelf -d libcheckcert.so来查看ELF的dynamic段:
BriansdeMacBook-Pro:armeabi-v7a brian$ arm-linux-androideabi-readelf -d libcheckcert.so Dynamic section at offset 0x19b80 contains 27 entries: Tag Type Name/Value 0x00000003 (PLTGOT) 0x1ad84 0x00000002 (PLTRELSZ) 1248 (bytes) 0x00000017 (JMPREL) 0x4200 0x00000014 (PLTREL) REL 0x00000011 (REL) 0x31a8 0x00000012 (RELSZ) 4184 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x6ffffffa (RELCOUNT) 390 0x00000006 (SYMTAB) 0x148 0x0000000b (SYMENT) 16 (bytes) 0x00000005 (STRTAB) 0x1028 0x0000000a (STRSZ) 6825 (bytes) 0x00000004 (HASH) 0x2ad4 0x00000001 (NEEDED) Shared library: [liblog.so] 0x00000001 (NEEDED) Shared library: [libc.so] 0x00000001 (NEEDED) Shared library: [libm.so] 0x00000001 (NEEDED) Shared library: [libstdc++.so] 0x00000001 (NEEDED) Shared library: [libdl.so] 0x0000000e (SONAME) Library soname: [libcheckcert.so] 0x0000000c (INIT) 0x4f9c 0x0000001a (FINI_ARRAY) 0x1a658 0x0000001c (FINI_ARRAYSZ) 8 (bytes) 0x00000019 (INIT_ARRAY) 0x1a660 0x0000001b (INIT_ARRAYSZ) 20 (bytes) 0x0000001e (FLAGS) BIND_NOW 0x6ffffffb (FLAGS_1) Flags: NOW 0x00000000 (NULL) 0x0
可以看到INIT和INIT_ARRAY节的地址分别为0x4f9c和0x1a660,打开IDA pro来查看相应位置的代码:
.text:00004F9C ; =============== S U B R O U T I N E ======================================= .text:00004F9C .text:00004F9C ; Attributes: bp-based frame .text:00004F9C .text:00004F9C EXPORT _init .text:00004F9C _init .text:00004F9C .text:00004F9C var_8 = -8 .text:00004F9C var_4 = -4 .text:00004F9C .text:00004F9C STMFD SP!, {R11,LR} .text:00004FA0 MOV R11, SP .text:00004FA4 SUB SP, SP, #8 .text:00004FA8 LDR R0, =(_GLOBAL_OFFSET_TABLE_ - 0x4FB4) .text:00004FAC ADD R0, PC, R0 ; _GLOBAL_OFFSET_TABLE_ .text:00004FB0 MOV R1, #4 .text:00004FB4 LDR R2, =(aBrian_1 - 0x1AD84) .text:00004FB8 ADD R2, R2, R0 ; "BRIAN" .text:00004FBC LDR R3, =(a_initEnter - 0x1AD84) .text:00004FC0 ADD R0, R3, R0 ; "_init enter" .text:00004FC4 STR R0, [SP,#8+var_4] .text:00004FC8 MOV R0, R1 .text:00004FCC MOV R1, R2 .text:00004FD0 LDR R2, [SP,#8+var_4] .text:00004FD4 BL __android_log_print .text:00004FD8 STR R0, [SP,#8+var_8] .text:00004FDC MOV SP, R11 .text:00004FE0 LDMFD SP!, {R11,PC} .text:00004FE0 ; End of function _init
init_array:0001A660 ; =========================================================================== .init_array:0001A660 .init_array:0001A660 ; Segment type: Pure data .init_array:0001A660 AREA .init_array, DATA .init_array:0001A660 ; ORG 0x1A660 .init_array:0001A660 DCD _Z13myConstructorv ; myConstructor(void) .init_array:0001A664 DCD sub_4E90 .init_array:0001A668 DCD sub_4EA8 .init_array:0001A66C DCD sub_4F04 .init_array:0001A670 DCB 0 .init_array:0001A671 DCB 0 .init_array:0001A672 DCB 0 .init_array:0001A673 DCB 0 .init_array:0001A673 ; .init_array ends
可以看到上面的代码中执行了我们所定义的函数,.init节就是_init函数的代码,而.init_array节是一个指针数组,每一项对应的是一块代码,可以做一系列的初始化操作。那么为什么.init节的代码先于.init_array节的代码执行呢?这个要看linker的代码了,位置在AOSP的bionic/linker目录下,这里只摘录里面的一小段代码:
void soinfo::CallConstructors() { ........ // DT_INIT should be called before DT_INIT_ARRAY if both are present. CallFunction("DT_INIT", init_func); CallArray("DT_INIT_ARRAY", init_array, init_array_count, false); }
可以看到先执行.init节中的代码,然后在顺序执行.init_array中的各个代码块。
到这里大家应该对_init函数、constructor属性及.init节和.init_array节的对应情况了解的很清楚了吧。
下面说一个我不太清楚的地方,用readelf查看ELF中的所有符号信息,可以看到在.rel.dyn和.rel.plt都有myConstructor符号,一个类型是R_ARM_ABS32一个是R_ARM_JUMP_SLOT。另外在IDA pro中查看myConstructor可以发现它的代码主体是在.text节,但是也可以发现在.plt和.got节中也有myConstructor的定义。这样的话每次显式调用myConstructor的时候都需要通过PLT来跳转然后从GOT表中来找到myConstructor在TEXT节中的真正地址才能执行。但在.init_array中的地址是它在TEXT节中的真正地址,初始化的时候调用myConstructor并不需要通过PLT和GOT表。不明白这是为什么?留待以后解决吧。
更新:上面这个问题是因为编译器的问题,不同的编译器编译出来的ELF文件是不太一样的,上面我说的这种情况是LLVM编译器编译出来的,而如果用arm-linux-androideabi-*的话myConstructor符号是只有在.text节中才有,不会出现在.rel.dyn和.rel.plt中。