第一次启动保护模式

来源:互联网 发布:万网域名注册官网 编辑:程序博客网 时间:2024/05/21 17:38

3.4.3 第一次启动保护模式

回到go_to_protected_mode()函数中,最后一行调用:

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

 

这个函数接受两个参数,第一个参数是grub传过来的保护模式的第一条代码。这个值就是前面说到了的,0x100000。后面这个就是给内核传递的参数,由于切换到了保护模式,所以要给出参数的线性地址,而不是有效地址,ds()函数就是ds寄存器的值。

 

来看看这个函数,它是由汇编语言实现的,代码在arch/x86/boot/pmjump.S中:

 

  23/*

  24 * void protected_mode_jump(u32 entrypoint, u32 bootparams);

  25 */

  26GLOBAL(protected_mode_jump)

  27        movl    %edx, %esi              # Pointer to boot_params table

  28

  29        xorl    %ebx, %ebx

  30        movw    %cs, %bx

  31        shll    $4, %ebx

  32        addl    %ebx, 2f

  33        jmp     1f                      # Short jump to serialize on 386/486

  341:

  35

  36        movw    $__BOOT_DS, %cx

  37        movw    $__BOOT_TSS, %di

  38

  39        movl    %cr0, %edx

  40        orb     $X86_CR0_PE, %dl        # Protected mode

  41        movl    %edx, %cr0

  42

  43        # Transition to 32-bit mode

  44        .byte   0x66, 0xea              # ljmpl opcode

  452:      .long   in_pm32                 # offset

  46        .word   __BOOT_CS               # segment

  47ENDPROC(protected_mode_jump)

 

要看懂上面的代码,需要用到一个C语言背景知识。在C语言中,假设咱们有这样的一个函数:

  int function(int a, int b){

        return a+b;

    }

 

调用时只要用result = function(1, 2)那样的方法就能够应用那个参数。但是,当高级语言被编译成电脑能够识别的机器码时,有一个疑问目就出现了:在CPU中,计算机没有办法清楚一个参数调用需求多少个、什么样的参数,也没有硬件能够保存这一些参数。也就是说,编译器不清楚怎么给那个参数传递参数,传递参数的任务必需由参数调用者和参数本身来协调。为此,使用栈来支持参数传递。

 

学过《数据结构》这么课程的童鞋都知道,栈是一种先进后出的Data框架,栈有一个存储区、一个栈顶指针。参数调用时,调用者依次把参数压栈,然后调用参数,参数被调用以后,在堆栈中取得Data,并停止计算。参数计算结束以后,或者调用者、或者参数本身改正堆栈,使堆栈还原原装。

 

在参数传递中,有两个很重要的东西目必需得到明确说明:

l  当参数个数多于1个时,按照什么顺序把参数压入堆栈

l  参数调用后,由谁来把堆栈还原原装

 

在编译器中,通过参数调用约定来说明这两个疑问,大多数编译器日常的调用约定有:stdcallcdeclfastcallthiscallnaked call

 

过去,stdcallgcc的默认方式,不过如今要显式地约定声明了:

    int __stdcall function(int a, int b)

 

stdcall的调用约定意味着:1)参数从右向左压入堆栈,2)参数自身改正堆栈 3)参数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。

 

以上述那个参数为例,参数a首先被压栈,然后是参数b,参数调用function(1, 2)。调用处汇编语言将变成:

    push 2 第二个参数入栈

    push 1 第一个参数入栈

    call function 调用参数,留意此时自动把cseip入栈

 

被调用参数_function处:

    push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,能够在出参数时还原

    mov ebp,esp 保存堆栈指针

    mov eax,[ebp+8H] 堆栈中ebp指向位置之前依次保存有ebpcs:eipab

                    所以ebp +8指向a

    add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b

    mov esp,ebp 还原esp

    pop ebp

    ret 8

 

而在编译时那个参数的姓名被中英对译成_function@8。从参数调用看,21依次被push进堆栈,而在参数中又经过相对于ebp(即刚进参数时的堆栈指针)的偏移量存取参数。参数结束后,ret 8意思是清理8个字节的堆栈,参数本人还原了堆栈。

 

fastcall调用约定和stdcall类似,它意味着:

 

参数的第一个和第二个DWORD参数(或者尺寸更小的)分别经过eaxedx传递,更多参数则经过从右向左的顺序压栈。过去的内核还有一个Gcc属性宏定义:

#define FASTCALL __attribute__((regparm(3)))

 

不过目前我看的2.6.34.1版本的内核的x86体系以及没有这个宏定义了,由此我推断,新的内核依赖的gcc新版本为了提升针对x86的性能,已经不再使用stdcallcdecl方案了,所有函数都是fastcall。为什么能提升了性能呢,你想想啊,x86eaxedx用于存放传入参数,作为高水平内核级程序员,函数的参数不应该超过2个。那么有人就会反过来问:超过2个的那些参数又何去何从?我的回答是:传入参数如果超过2个,多余的参数还是被存放到局部栈中,表面上没什么不好,但是不管是系统调用,还是系统陷阱都会引起用户空间陷入内核空间。我们知道,系统空间的权限级是0,用户空间的权限级为3,系统调用从权限级为3的用户空间陷到权限级为0的内核空间,必然引起堆栈切换,linux系统将从全局任务状态栈TSS中找到一个合适的内核栈信息保存覆盖当前SPSS两个寄存器的内容,以完成堆栈切换,此时处于内核空间所看到的栈已不是用户空间那个栈,所以在调用的时候压入用户栈的数据就在陷入内核的那个瞬间,被滞留在用户空间栈,内核根本不知道它的存在了,所以作为安全考虑或者作为高水平程序员的切身修养出发,都不应该向系统调用级函数传入过多的参数。

 

一边分析内核,一边学习大量的计算机与编程知识,这就是内核给我们带来的巨大乐趣,何乐而不为呢?我们继续。刚才介绍了带参数函数调用的参数传递的C语言方面的知识,现在我们知道了,来到protected_mode_jumpeax存放的是code32_start,其值是header.S中的152行定义的hdr传递过来的0x100000edx存放的是bootparams参数32位的地址。

 

27行:

       movl       %edx, %esi           # Pointer to boot_params table

 

执行此指令后esi寄存器存放着boot_params结构的首地址,随后29~32行:

       xorl %ebx, %ebx

       movw     %cs, %bx

       shll  $4, %ebx

       addl %ebx, 2f

       jmp  1f                  # Short jump to serialize on 386/486

 

执行上述指令后,ebx寄存器存放的就是2f程序段对应的地址值。为了学习,这里不得不多说几句。cs16位的,而我们马上要进入保护模式了,不管是代码寻址还是数据寻址,都是32位的了,此时此刻的这个cs所指向的段没有什么意义了,所以上面的动作就是将进入保护模式代码段了之前的cs:eip的值保存在ebx中,保存来干嘛呢,马上会用到。

 

继续走:34~44行:

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

 

这段代码就是protected_mode_jump的核心,即打开cr0PE位,打开保护模式。由于:

#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)

 

所以16cx的值是(2*8+1*813616进制为0x88di为(2*8+2*814416进制为144eax存放的0x100000edxesi存放了boot_params的首址;还有一个ebx存放这向保护模式切换前cs:eip指令地址。所以即将进入保护模式前夕,这几个寄存器的值就是这个样子滴。

 

在执行完第40行代码后,内核从此告别实模式,开始了x86的保护模式之旅。注意,从系统启动到此,并不是第一次进入保护模式,前面bootloader阶段,grub已经执行过一下保护模式的命令了,不然怎么会把vmlinuz第三部分的代码拷贝到内存0x100000之后呢。随后立即跳到45行标号2,开始执行保护模式下的代码:

 

2:    .long       in_pm32                # offset

       .word      __BOOT_CS         # segment

 

在函数main的最后,实际上就是已关中断,准备进入保护模式,设置最初始的gdt,idt等。当前面执行.byte 0x66, 0xea指令时,cs寄存器的值被设置为__BOOT_CS,即代码段选择子,偏移量就是子程序名in_pm32。至于如何的到对应的物理地址,请查看“保护模式编程”

 

来看子程序in_pm32

  49        .code32

  50        .section ".text32","ax"

  51GLOBAL(in_pm32)

  52        # Set up data segments for flat 32-bit mode

  53        movl    %ecx, %ds

  54        movl    %ecx, %es

  55        movl    %ecx, %fs

  56        movl    %ecx, %gs

  57        movl    %ecx, %ss

  58        # The 32-bit code sets up its own stack, but this way we do have

  59        # a valid stack if some debugging hack wants to use it.

  60        addl    %ebx, %esp

  61

  62        # Set up TR to make Intel VT happy

  63        ltr     %di

  64

  65        # Clear registers to allow for future extensions to the

  66        # 32-bit boot protocol

  67        xorl    %ecx, %ecx

  68        xorl    %edx, %edx

  69        xorl    %ebx, %ebx

  70        xorl    %ebp, %ebp

  71        xorl    %edi, %edi

  72

  73        # Set up LDTR to make Intel VT happy

  74        lldt    %cx

  75

  76        jmpl    *%eax                   # Jump to the 32-bit entrypoint

  77ENDPROC(in_pm32)

 

53~57行首先把dsesfsgsss设置成同样的值,即__BOOT_DS,其值为0x88。为什么是0x88呢?注意,我们分析代码的目的是学习,是研究,不是其他的,所以来研究一下。我们知道,进入保护模式后,段寄存器的值就不再是存放段基址了,而是存放段选择子。

 

选择子的格式如下:(来自博客“Intel 80286工作模式”

 

 

 

 

0x88正好就是二进制的10001000,其中10001对应选择子的偏移量D15D3,即所要访问的描述子在描述子表中的偏移量。RPL00,对应最高特权级(初始化阶段,特权级当然最高),TI0,表示全局描述符。

 

好,继续研究。那么偏移量为啥要设置成10001呢?回顾一下,我们前面是如何安装全局描述符表的。setup_gdt()函数中,我们建立了一个全局描述符表boot_gdt,然后把这个表的首地址和长度加载到GDTR寄存器中。这个表的每个成员是64位,即8个字节。10001换算成十进制就是17,也就是经历了28字节后的偏移位置。我为什么说是28字节,不错,看看那个表的定义,第38字节,正是从GDT_ENTRY_BOOT_DS下标开始的。所以,经过这样的一系列指令后,dsesfsgsss这五个寄存器的内容都是指向全局描述符表boot_gdt的数据段了。

 

继续走,60行对进入保护模式后的堆栈进行调整,将原来的栈顶指针esp加上刚才保存在ebx的代码段偏移。注意,这个地方注释上写得很清楚了,32位保护模式有自己的堆栈,这里调整堆栈的目的是供某些黑客娱乐的。

 

63行,执行ltr指令,目的在于设置TSS相关的TR寄存器。至于想学习TSS内容的朋友,请参考博客“Intel 80286工作模式”6771行清空刚才使用的那些通用寄存器。最后74行加载LDTR寄存器设置局部描述符表。此时ecx的值为空,所以这步操作是一个没有意义的操作。

 

最后一行,76行执行jmpl *%eax,开始执行由函数参数方式传递进来的code32_start,即0x100000处的代码,我们著名的“文艺复兴时期”。前面讲过,在解压缩vmlinuz之前,这代码在arch/x86/boot/compressed/head_32.S中,入口是ENTRY(startup_32)

 

由于段的基地址为0(可以参考go_to_protected_mode()中的setup_gdt()函数),所以线性地址等于有效地址,因为目前还没有分页,所以线性地址也其实就是物理地址,物理地址1M后正是保护模式代码所在地)

 

 

原创粉丝点击