【Linux内存源码分析】初探保护模式(3)

至此linux首次进入保护模式所需的准备工作已经基本完成,段描述符表准备好了,而且GDTR也设置完毕了。

那么接下来看一下go_to_protected_mode()最后的调用:

protected_mode_jump(boot_params.hdr.code32_start,
(u32)&boot_params + (ds() << 4));

这是由C语言跳转到汇编的一个调用,protected_mode_jump是纯汇编实现的一个函数:

【file:/arch/x86/boot/pmjump.s】
/*
 * void protected_mode_jump(u32 entrypoint, u32 bootparams);
 */
GLOBAL(protected_mode_jump)
	movl	%edx, %esi		# Pointer to boot_params table

	xorl	%ebx, %ebx
	movw	%cs, %bx
	shll	$4, %ebx
	addl	%ebx, 2f
	jmp	1f			# Short jump to serialize on 386/486
1:

	movw	$__BOOT_DS, %cx
	movw	$__BOOT_TSS, %di

	movl	%cr0, %edx
	orb	$X86_CR0_PE, %dl	# Protected mode
	movl	%edx, %cr0

	# Transition to 32-bit mode
	.byte	0x66, 0xea		# ljmpl opcode
2:	.long	in_pm32			# offset
	.word	__BOOT_CS		# segment
ENDPROC(protected_mode_jump)

	.code32
	.section ".text32","ax"
GLOBAL(in_pm32)
	# Set up data segments for flat 32-bit mode
	movl	%ecx, %ds
	movl	%ecx, %es
	movl	%ecx, %fs
	movl	%ecx, %gs
	movl	%ecx, %ss
	# The 32-bit code sets up its own stack, but this way we do have
	# a valid stack if some debugging hack wants to use it.
	addl	%ebx, %esp

	# Set up TR to make Intel VT happy
	ltr	%di

	# Clear registers to allow for future extensions to the
	# 32-bit boot protocol
	xorl	%ecx, %ecx
	xorl	%edx, %edx
	xorl	%ebx, %ebx
	xorl	%ebp, %ebp
	xorl	%edi, %edi

	# Set up LDTR to make Intel VT happy
	lldt	%cx

	jmpl	*%eax			# Jump to the 32-bit entrypoint
ENDPROC(in_pm32)

其实C语言调用跳转汇编函数没什么特殊的,就call指令就完了。真正要理解这段汇编的实现,侧重要了解参数的传递方式。

我们通常都知悉Intel通常都是通过压栈传参的,以至于gdb调试程序时,可以通过bt查看到各个函数调用时的传参信息。如果要是有接触过ARMMIPSPPC处理器底层的程序开发的话,大体都知道它们的参数是通过寄存器传递的。比如ARM处理器,C语言函数调用时,通常将R0-R34个寄存器存储参数04的值传递到子函数中,如果参数超过4个则多余的参数将会压栈传递,而R0还会用来作为子函数返回值传递回去。而在内核中/arch/boot下面的代码也采用了类似的寄存器传递参数的方式,三个及以内的参数分别依序以eaxedxecx作为03的入参,如果超过3个,这会采用压栈传参。

既然已经知悉传参方式了,那么接下来看一下代码实现:

movl                      %edx, %esi

这里是把入参boot_params的地址保存到esi中,而自此esi就不再在此pmjump.s的汇编代码中出现,所以可以推测这个是用来以备后用的,它不是这里的关键数据。

紧接着:

xorl                      %ebx, %ebx

movw                      %cs, %bx

shll                      $4, %ebx

addl                      %ebx, 2f

ebx清空后,把“2: .long  in_pm32”的物理地址保存到ebx上。待会儿再讲一下它的作用。

接着往下看:

movw                      $__BOOT_DS, %cx

movw                      $__BOOT_TSS, %di

代码里找一下__BOOT_DS__BOOT_TSS的定义:

【file:/arch/x86/include/asm/segment.h】
#define GDT_ENTRY_BOOT_CS	2
#define __BOOT_CS		(GDT_ENTRY_BOOT_CS * 8)

#define GDT_ENTRY_BOOT_DS	(GDT_ENTRY_BOOT_CS + 1)
#define __BOOT_DS		(GDT_ENTRY_BOOT_DS * 8)

#define GDT_ENTRY_BOOT_TSS	(GDT_ENTRY_BOOT_CS + 2)
#define __BOOT_TSS		(GDT_ENTRY_BOOT_TSS * 8)

不难看出这是前面提到的段描述符表项的索引值GDT_ENTRY_BOOT_DS,而8则是2^3,这里不是什么大小乘法,而是起到位移的作用,左移3位。因为段寄存器低端有3bit是预留给了TIRPL的。然后把段值存到cx寄存器中,这个后面会用到的。

好了,到关键代码了:

movl                      %cr0, %edx

orb                       $X86_CR0_PE, %dl  # Protected mode

movl                      %edx, %cr0

可以看到将cr0的值暂存到edx中,然后将edx对应cr0PE位进行设置,最后把edx设置到cr0上,至此,随着cr0PE被置位将保护模式开启。开启后就到了:

                          .byte 0x66, 0xea     #
ljmpl opcode

2:                        .long in_pm32       #
offset

                          .word __BOOT_CS      #
segment

指令其本质就是数据,这些数据就构造成了一个长跳转指令,跳转的目的地址是“__BOOT_CSin_pm32”segmentoffset),也就是将会跳转到GLOBAL(in_pm32)去执行下面的汇编指令:

movl                      %ecx, %ds

movl                      %ecx, %es

movl                      %ecx, %fs

movl                      %ecx, %gs

movl                      %ecx, %ss

好了,这里就看到刚才cx寄存器保存的值的作用了。它是用来设置各个段寄存器的,貌似少了cs寄存器的设置?非也,cs寄存器随着刚才的那个长跳转已经设置上去了,所以就没有必要做重复工作。

接下来顺带提一下ebx的用途:

addl                      %ebx, %esp

它是用来把地址设置给esp,栈寄存器。不过为什么指向代码段呢?根据注释可以了解之所以需要设置栈位置,是为了调试用的。但是至于指向代码段,这是由于这段代码只会执行一次,所以没有存在的意义,就当做废物利用吧,应该是这个意图。

完了,后面的汇编就不再详述了,至此已经进入保护模式了。整个过程看起来有点绕,实际上这是跟随Intel的手册说明来实现的,看一下手册关于进入保护模式的描述:

Switching to Protected Mode
Before switching to protected mode from real mode, a minimum set of system data structures and code modules must be loaded into memory, as described in Section 9.8, “Software Initialization for Protected-Mode Operation.” Once these tables are created, software initialization code can switch into protected mode.
Protected mode is entered by executing a MOV CR0 instruction that sets the PE flag in the CR0 register. (In the same instruction, the PG flag in register CR0 can be set to enable paging.) Execution in protected mode begins with a CPL of 0.
Intel 64 and IA-32 processors have slightly different requirements for switching to protected mode. To insure upwards and downwards code compatibility with Intel 64 and IA-32 processors, we recommend that you follow these steps:
1.	Disable interrupts. A CLI instruction disables maskable hardware interrupts. NMI interrupts can be disabled with external circuitry. (Software must guarantee that no exceptions or interrupts are generated during the mode switching operation.)
2.	Execute the LGDT instruction to load the GDTR register with the base address of the GDT.
3.	Execute a MOV CR0 instruction that sets the PE flag (and optionally the PG flag) in control register CR0.
4.	Immediately following the MOV CR0 instruction, execute a far JMP or far CALL instruction. (This operation is typically a far jump or call to the next instruction in the instruction stream.)
5.	The JMP or CALL instruction immediately after the MOV CR0 instruction changes the flow of execution and serializes the processor.
6.	If paging is enabled, the code for the MOV CR0 instruction and the JMP or CALL instruction must come from a page that is identity mapped (that is, the linear address before the jump is the same as the physical address after paging and protected mode is enabled). The target instruction for the JMP or CALL instruction does not need to be identity mapped.
7.	If a local descriptor table is going to be used, execute the LLDT instruction to load the segment selector for the LDT in the LDTR register.
8.	Execute the LTR instruction to load the task register with a segment selector to the initial protected-mode task or to a writable area of memory that can be used to store TSS information on a task switch.
9.	After entering protected mode, the segment registers continue to hold the contents they had in real-address mode. The JMP or CALL instruction in step 4 resets the CS register. Perform one of the following operations to update the contents of the remaining segment registers.
—	Reload segment registers DS, SS, ES, FS, and GS. If the ES, FS, and/or GS registers are not going to be used, load them with a null selector.
—	Perform a JMP or CALL instruction to a new task, which automatically resets the values of the segment registers and branches to a new code segment.
10.	Execute the LIDT instruction to load the IDTR register with the address and limit of the protected-mode IDT.
11.	Execute the STI instruction to enable maskable hardware interrupts and perform the necessary hardware operation to enable NMI interrupts.
Random failures can occur if other instructions exist between steps 3 and 4 above. Failures will be readily seen in some situations, such as when instructions that reference memory are inserted between steps 3 and 4 while in system management mode.

可以看出来这个实现也不算是太复杂,是跟随着intel的手册指引来实现的。

One reply

  1. calvin说道:

    ljmpl是本地跳转的意思吗?如果按照把in_pm32装载到EIP,__BOOT_CS加载到cs,那么跳转到in_pm32的链接地址。但是bootloader加载setup.bin的位置是不确定的,链接地址跟运行时地址不一致。
    我感觉这个ljmpl不是直接加载EIP,应该像是一个本地跳转(用符号差值作为向后跳转的偏移量),额外带有加载段寄存器的效果。

发表评论

电子邮件地址不会被公开。 必填项已用*标注