1.对符号表的更进一步说明
首先是两段代码
test.c:
#include<stdio.h>
void func()
{
printf("fuck zsr\n");
return 0;
}
code.c:
#include<stdio.h>
void func();
int main()
{
func();
return 0;
}
对test.o进行readelf -s 测试有
cpp
ubuntu@VM-0-2-ubuntu:~/test$ readelf -s test.o
Symbol table '.symtab' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata
4: 0000000000000000 26 FUNC GLOBAL DEFAULT 1 func
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
能发现printf的底层就是puts,有些函数的Ndx是UND,即未定义状态。链接时的操作就是每个文件拿自己未定义的函数去其他文件中进行匹配。
当Ndx为一个准确的值时,代表的意思是其处于的第几个section中。
分别对.o和.exe进行objump -d 操作,能发现曾经未被初始化地址的地方已经初始化了
ubuntu@VM-0-2-ubuntu:~/test$ objdump -d test.o
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <func>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <func+0xf>
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <func+0x17>
17: 90 nop
18: 5d pop %rbp
19: c3 ret
ubuntu@VM-0-2-ubuntu:~/test$ objdump -d test
//..
0000000000001162 <func>:
1162: f3 0f 1e fa endbr64
1166: 55 push %rbp
1167: 48 89 e5 mov %rsp,%rbp
116a: 48 8d 05 93 0e 00 00 lea 0xe93(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1171: 48 89 c7 mov %rax,%rdi
1174: e8 d7 fe ff ff call 1050 <puts@plt>//e8能说明与上面是同一个函数,这里
//成功初始化了地址
1179: 90 nop
117a: 5d pop %rbp
117b: c3 ret
//..
地址重定位:链接过程中对.o中的外部符号的地址进行更新(这也就是.o文件名:可重定位目标文件的由来),直接意思就是该文件的内部符号地址可被修改
2.EIF加载
一个.exe,即使没有被加载到内存当中,其也是依旧有地址的。
原因:
(1)EIF的每一个区间都是由其自己的起始地址的(甚至于EIF相对于磁盘整体也是有自己的起始地址的),里面的所有成员的存储位置都是通过该区域的起始地址+偏移量记录的,这种起始地址+偏移量存储地址的方式叫逻辑地址,在进程的mm_struct中寻找变量或函数地址的方式就是通过逻辑地址来调用访问的。
(2)其实从OS看来,整个.exe其实就是一个segment(也就是说OS将一个.exe视为一个整体),对于这个segment而言的起始地址其实为0,也就是说真实情况下EIF的每个成员的地址其实就是偏移量。
(3)EIF编址从0开始编,但不代表要从0为开始用,从某一位开始到最后依次递增编址,这种编址方式叫平坦模式编址(虽然连续,但其实每个区间的地址编号之间不一定连续)
(4)其实磁盘上的EIF得编址就是进程中虚拟地址得编制方式(其实应该说虚拟地址得初始化其实就是EIF编址得数据)
(5)由于磁盘加载进内存与编译器有关,而虚拟地址得形成与OS有关,因此可以说虚拟地址即影响着OS得编制方式,也影响着编译器的编制方式。
(6)说白了内存和磁盘都是采用得平坦模式编址,只是在内存和磁盘中的叫法不同。磁盘中叫其逻辑地址,内存中叫其虚拟地址。故也可以推测进程中的mm_struct和vm_area_struct的数据的初始化其实就是用的EIF中的逻辑地址的数据。
3.EIF中的逻辑地址
所以合并完后是要对整体的section地址进行重新编址的。
总结静态链接的操作
合并.o,重新编址,更新函数地址.
(1).exe在磁盘中存的就是指令汇编语言的二进制机器码。
(2).exe的数据会直接加载到内存中,然后通过内存中每个区间的数据就可以直接初始化到mm_struct中了。
(3)每一行代码都是真实加载到物理内存中的,所以每一行代码在内存中也会有一个真实地址(真实地址与虚拟地址的值增长方向相同),因此一个指令就有两个地址,页表就可以将两个地址加载在页表的两边形成映射关系了。
4.CPU找到.exe的起始物理地址的过程

EIF header中有一个成员Entry point address存储着该.exe的起始虚拟地址该成员在.exe加载进内存是会将其存储的值加载到CPU中一个叫EIP的寄存器中(当CPU指向一个进程时,CR3指向该进程的页表,MMU是用与寻址的),因此CPU能通过EIP和MMU找到EIP指向的真实地址,并将对应的指令加载到寄存器中。
因此进入CPU的地址只是虚拟地址,(EIP+MMU转换找到真实地址就直接去找指令了,并不会返回给CPU),所以CPU并不知道真实地址的存在,其看待内存中的.exe和磁盘中的内容是一模一样的。

不同的segment分别映射为一个vm_area_struct,一个vm_area_struct对应虚拟地址的一个区间,因此不同虚拟地址的区分本质就是不同的segment而已。
总结:
进程先形成PCB,mm_struct,vm_area_struct和页表,通过struct file,找struct path,再找struct inode,最后找到数据块,之后加载磁盘的数据进内存,并用于初始化mm_struct,最后构建页表的映射关系。
EIF中存的地址其实就是_start函数的地址,也就是程序开始时的虚拟地址。