Kernel 2.4.0 之 head.S 为何用两次 jmp 刷新 EIP 寄存器(2)

这里为何要跳转两次?情景分析里说的理由太过牵强,书中解释也是令人费解。其实进行两次jmp纯粹是多余的,仅靠其中一次跳转就能完成任务。而经过我的实验与研究,我发现这两次跳转完全可以全部删掉,根本不影响系统的启动。

为什么这么说呢? 在讲解原因之前,必须先说点Intel处理器的规定,因为待会儿要看汇编语言和机器语言的代码才能彻底弄明白一切。

jmp跳转分为远跳转(far)和近跳转(near and short),远跳转是指覆写CS的跳转,近跳转是指不重写CS的跳转。

近跳转又分两种:

绝对跳转(absolute)和相对跳转(relative),绝对跳转在汇编里的写法是 jmp register/memory-location ,即跳转的目的地址存储在寄存器内或内存位置内,CPU直接把这个目的地址覆写到EIP中,EIP=absolute_address;

相对跳转的写法是 jmp label ,汇编语言中一般写作跳到某个标号label,在机器语言层面上这个标号被汇编成一个叫做relative offset的立即数。即jmp后面的数字是一个相对偏移量,CPU将这个偏移量加到EIP上去产生目的地址,EIP=EIP+offset。注意当CPU正在执行jmp指令时,EIP指向jmp的后一条指令,所以这个相对偏移就是jmp后一条指令的地址到目的地址之间的差值,(跳转的目的地址)-(jmp后一条指令的地址)= offset。

在机器语言层面上:

绝对跳转的机器码是ff,后面的操作数代表目的地址存放的位置,比如e0代表eax寄存器,那么ffe0就表示将eax中的目的地址数值取出来,直接覆写至EIP寄存器,下一次取指令就从目的地址取了。

相对跳转的机器码是eb,后面的操作数是相对偏移,在汇编器进行汇编操作时会自动进行运算:(跳转的目的地址)-(jmp后一条指令的地址)= offset,将这个offset放在eb后面作为操作数,CPU执行jmp跳转时EIP恰好指向jmp的后一条指令处,CPU将offset操作数加到EIP上恰好得到跳转的目的地址,然后EIP中就是目的地址了,下一次取指令就从目的地址取了。

在内核汇编完成的链接阶段,arch\i386\Vmlinux.lds文件第9行 . = 0xC0000000 + 0x100000; 说明在ld链接时给最终的vmlinux文件里面所有的符号地址都加上0xC0000000 + 0x100000,也就是都加上0xC0100000。这个操作对相对跳转没有任何影响,因为相对跳转在机器码层面的操作数是相对偏移,不管目的地址和jmp后一条指令的地址被链接器改成了多少,这俩地址的差是不变的,也就是说相对偏移不会被链接器所影响,它永远是个差。转而看绝对跳转就不一样了,绝对跳转的目的地址存储在寄存器或内存里,那在 jmp *%eax 之前必然要 mov label,%eax ,这个label不是相对偏移了,它切切实实是某条指令的绝对地址,因为这里并不是jmp相对跳转指令! 那它既然是一个绝对地址,链接器就会给它统一加上一个值0xC0100000,这必然会影响jmp指令,如果原来label代表地址0x42,即jmp是往0x42跳的话,那现在label变成了0xC0100042,jmp就是往0xC0100042跳转。记住,只有jmp相对跳转指令后面的数字才是相对偏移,链接器无法将之修改,其他指令中的标号全部是绝对地址,是可以被链接器修改的!

下面终于要开始看这两条jmp指令的作用了。源汇编代码如下:

movl %cr0,%eax orl $0x80000000,%eax movl %eax,%cr0 /* ..and set paging (PG) bit */ jmp 1f /* flush the prefetch-queue */ 1: movl $1f,%eax jmp *%eax /* make sure eip is relocated */ 1: /* Set up the stack pointer */ lss stack_start,%esp

我们再来看看内核的反汇编代码。在顶层Makefile里讲 CFLAGS_KERNEL = 改为 CFLAGS_KERNEL = -g 给内核加入调试信息,然后 objdump -d vmlinux | less 反编译内核镜像vmlinux的结果如下:

虚拟地址: 物理地址: c010002e: 10002e 0f 20 c0 mov %cr0,%eax c0100031: 100031 0d 00 00 00 80 or $0x80000000,%eax c0100036: 100036 0f 22 c0 mov %eax,%cr0 c0100039: 100039 eb 00 jmp c010003b <_text+0x3b> c010003b: 10003b b8 42 00 10 c0 mov $0xc0100042,%eax c0100040: 100040 ff e0 jmp *%eax c0100042: 100042 0f b2 25 e4 01 10 c0 lss 0xc01001e4,%esp

可以看到在内核编译链接完成后所有符号的地址都变成了0xC0100000之后的数,数值上讲都大于3GB,毕竟内核空间的范围是虚拟地址空间的3G-4G。我为了方便,把物理地址也标上了。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/54af4a66bd5ed00cc37642fae3cbd739.html