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

在arch\i386\kernel\head.S文件中,自line 100开始有这么几行:

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

我看了很久都不明白第一次跳转到底是为了什么,情景分析那本书上说这是为了刷新指令预取队列,我把Intel手册翻了个遍也没找到关于预取队列的详细信息,维基百科上介绍的也不够详细。

在我终于弄明白之后,写一写我的分析过程,这份文章写写改改,用时一下午加一晚上才完工,一边写一边发现了很多自己得过且过的问题,写文章的过程也是查资料的过程,还是比较累的,如果写的有错误,欢迎指出。下面分析的过程也是思考的过程。

首先从setup.S看起,在arch\i386\boot\setup.S中line 113处有这么几行代码:

code32_start: # here loaders can put a different # start address for 32-bit code. #ifndef __BIG_KERNEL__ .long 0x1000 # 0x1000 = default for zImage #else .long 0x100000 # 0x100000 = default for big kernel #endif

因为我们编译的是bzImage,所以code32_start标号处的数值为0x100000,占用四字节。

再看line 532处的几行代码:

# we get the code32 start address and modify the below 'jmpi' # (loader may have changed it) movl %cs:code32_start, %eax movl %eax, %cs:code32

在执行这些代码时CPU还处于实模式,所以CS里面是段基址,不是selector!第一句是把code32_start处的一个双字(四字节)装入eax,这个双字的值就是0x100000;然后第二句把eax即0x100000赋值到code32标号所指的内存位置里。那么这个位置在哪呢?请继续看下面line 719的代码:

# NOTE: For high loaded big kernels we need a # jmpi 0x100000,__KERNEL_CS # # but we yet haven't reloaded the CS register, so the default size # of the target offset still is 16 bit. # However, using an operant prefix (0x66), the CPU will properly # take our 48 bit far pointer. (INTeL 80386 Programmer's Reference # Manual, Mixing 16-bit and 32-bit code, page 16-6) .byte 0x66, 0xea # prefix + jmpi-opcode code32: .long 0x1000 # will be set to 0x100000 # for big kernels .word __KERNEL_CS #这个数字是0x10

0x100000这个数字最终被写到了code32这个标号处,覆盖了原来的0x1000。那么当执行到line 719时会发生什么呢?

0xea这个数字其实是jmpi指令的机器码,而0x66则告诉处理器jmpi要按照保护模式的方式来取操作数,即先取出一个4字节的双字操作数置入EIP,然后继续取出一个2字节的字操作数置入CS。如果不加0x66前缀那么jmpi指令只会取2字节的操作数置入EIP,显然这是不对的。至于为什么这个前缀是0x66,这个问题要去问Intel了。注意当代码执行到此,code32处的值早已经被覆盖成了这个样子:

code32: .long 0x100000 .word __KERNEL_CS #这个数字是0x10

所以jmpi指令会先后取出0x100000和0x10分别置入EIP和CS。如果将这几行用伪代码来表示,既然0xea是jmpi的机器码,0x66是前缀,我们姑且创造一条新的汇编指令pjmpi,那么上面几行表示出来就是这样的:

pjmpi 0x100000,0x10

这样就很清晰了,0x100000置入EIP,0x10置入CS。 到此为止,CS里面的数值0x10就是selector,对应的描述符中指明该代码段的基地址为0,又因为EIP=0x100000,所以经过分段机制后可得线性地址为0x100000,数值上没变。此时尚未开启分页机制,该线性地址当作物理地址,它被送上地址总线准备从此处取指令。那么0x100000这个地址处能取出什么指令呢?

物理地址0x100000这个数值其实是1MB处。那里就是内核的主代码,也就是head.S的入口点startup_32。于是CPU会取出head.S中的第一条指令开始执行,往后就是继续执行head.S剩余的部分了。

看到这里必须明确:CS中是_KERNELCS代码段selector,EIP中的虚拟地址值虽然需要经过分段机制才能当作物理地址,但是段基址为0,对数值没影响,物理地址和虚拟地址数值上相等。每次取指令后EIP自动增加一个数,这个数就是刚才取的指令的长度,靠这种方式EIP从虚拟地址0x100000开始递增,逐次取指令执行指令。

进入head.S后,从startup32入口开始的执行过程如下:先将数据段选择子KERNELDS置入ds等寄存器,然后设置好页表的内容,而页目录表的内容是直接写到head.S文件中的,这样页目录表和页表都��备了,再然后就是将页目录表的物理地址置入cr3寄存器,再将cr0的PG标志位置1,从此分页机制开启了!

紧接着就是刷新指令预取队列的代码了,自line 103开始就是这几行令人费解的代码了:

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

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

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