链接器在合并各个目标文件中的段时需要将标识符的最终地址给确定了。这就是重定位的过程(确定各个段的最终起始地址以及各个标识符的地址)。
示例:
test.c如下:
func.c如下:
编译func.c,并查看符号信息:
可以看到func这个标识符相对于代码段的偏移位置为0。
g_pointer前面有一个C标志,意思是暂时还不知道该将g_pointer这个标识符放在哪一个段。能知道的就是这个标识符的大小为4个字节,就是前面显示的00000004。
标识符的具体地址是没有确定的,因此目标文件不能执行。
编译test.o:
上图中的func意味着在test.c文件中用到了func这个标识符,但是不是在test.c中定义的。这个标识符具体位于哪里是不清楚的。
g_pointer和printf是同样的道理。
这三个表示符都不是在test.c中定义的。
上图中还可以看出,g_global位于bss段(由前面的B指示),相对于bss段的起始地址为0.
g_test位于data段(由前面的D标识),相对于data段的偏移为0.
main位于text段,具体地址不确定。
开始进行链接:
nm查看符号:
链接之后这些符号都有了地址。
链接器对这些符号进行了重定位(指的是给段中的各个标识符一个最终的地址)。
上面的情况是针对linux的(gcc编译)。
实验:
程序正常执行了。
使用objdump反编译:
得到的文件内容如下:
找到main函数:
在第68行发现了。
这意味着main可能被其他函数调用了。
上图中,在start将main函数的地址压入栈中,然后调用libc_start_main函数,其中压入栈中的main函数的地址作为libc_start_main的参数。
证明第一个被调用的函数是start:
我们只需要查看这个函数是不是在text段的起始地址处。
使用objdump -h查看可执行文件:
可以看到start函数的地址正好是text段的起始地址。
linux下程序加载的过程:
创建虚存空间。
将对应的段从可执行程序文件里面拷贝到内存当中。
执行程序(将pc指针指向代码段的第一条指令)。
上图中第65行将一个地址0x8048420压入栈中,现在我们来查找一下这个地址:
可以看到这个地址是libc_csu_init函数的地址,这个函数里面又调用了init函数。
接着查找init函数:
可以看到第20行调用了一个全局的构造函数(不是c++中的构造函数)。
经过分析可知,lic_start_main做了一些环境初始化的工作。
反汇编中第64行就是清理函数地址。
启动流程总结:
main函数只是c语言的入口函数,而不是整个程序加载执行的入口。
程序如下:
先编译生成目标文件program.o。再执行链接。
正常执行和退出了。
使用nm查看符号:
使用objdump -h查看:
可以看到program的地址正好是text段的起始地址。可以看到没有了系统提供的启动函数。
使用了nostartfiles选项就不会使用系统中的启动文件。