Chinaunix首页 | 论坛 | 博客
  • 博客访问: 967127
  • 博文数量: 403
  • 博客积分: 27
  • 博客等级: 民兵
  • 技术积分: 165
  • 用 户 组: 普通用户
  • 注册时间: 2011-12-25 22:20
文章分类

全部博文(403)

文章存档

2016年(3)

2015年(16)

2014年(163)

2013年(222)

分类: LINUX

2013-05-18 14:50:51

1问题

使用GRUB2不能正确引导X86_64 linux-2.6.21 bzImage内核。为解决这一问题,本文展开研究GRUB2加载方式,X86_64 bzImage生成结构、解压缩和启动过程。

 

2 X86_64 bzImage生成结构

bzImage的生成步骤如下:

1) 内核vmlinux是由arch/x86_64/kernel/vmlinux.lds文件链接规则由LD链接而成的elf64

式文件;本文称之为第一个vmlinux

2) objcopyvmlinux转成二进制的vmlinux.bin;本文称之为第一个vmlinux.bin

3) gzipvmlinux.bin压缩成vmlinux.bin.gz

4) vmlinux.bin.gzarch/x86_64/boot/compressed/vmlinux.scr链接规则链接成piggy.o,此文件只有数据段;vmlinux.scr链接规则如下:

SECTIONS

{

  .data : {

     input_len = .;

     LONG(input_data_end - input_data) input_data = .;

     *(.data)

     input_data_end = .;

     }

}

5) arch/x86_64/boot/compressed/下的misc.c, head.S以及生成的piggy.o链接成另外一个压缩vmlinux elf32格式的文件,head.Smisc.c是为了解压piggy.o中包含的vmlinux.bin.gz所用;本文称之为第二个vmlinux

6) objcopy将压缩vmlinux转成vmlinux.bin二进制格式文件;本文称之为第二个vmlinux.bin

7) arch/x86_64/boot/tools/build.c编译生成build程序,arch/x86_64/boot/bootsect.Sarch/x86_64/boot/setup.S编译生成二进制类型的bootsectsetup程序;

8) build程序将setupbootsect二进制格式文件,以及步骤6)中生成的vmlinux.bin二进制格式文件打包成一个最终的bzImage.

 

因此,bzImage组成结构:

1) bzImage: bootsect+setup+vmlinux.bin

2) vmlinux.binhead.S+misc.c+vmlinux.bin.gz

3) vmlinux.bin.gz: gzip压缩的vmlinux.bin即最终的内核

 
3 GNU LD用法

一个C程序的生成过程包括:预处理、编译、汇编和链接过程。

Linux下的链接过程是由LD完成的,它可以控制生成目标程序的格式,以及各个部分的组成特点,包括加载地址(LMA)和运行地址(VMA),这些行为可以通过配置链接文件完成,可理解为链接文件是LD程序执行的参数配置文件,决定了链接出来的目标文件的组成;程序由许多节(section)组成,因此链接文件主要是围绕节的定义展开的。

本小节通过简单分析arch/x86_64/kernel/vmlinux.lds.S链接文件来介绍LD的用法:

a) OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64")

  OUTPUT_FORMAT命令表示生成的目标默认格式,大端字节序时的目标格式以及小端

字节序时的目标格式;

b) OUTPUT_ARCH(i386:x86-64)

  OUTPUT_ARCH表示特定的输出机器架构

c) ENTRY(phys_startup_64)

  ENTRY表示目标程序的入口地址;

有多种不同的方法来设置入口点,连接器会通过按顺序尝试以下的方法来设置入口点,

如果成功了就会停止:

* ‘-e’入口命令行选项

* 连接脚本中的ENTRY(SYMBOL)命令

* 如果定义了start,就使用start的值

* 如果存在,就使用’.text’节的首地址

* 地址0

d) SECTIONS{}含有目标程序的各个节的定义

e) 节中的定义:. = __START_KERNEL;

      .在链接文件表示一种特殊的符号,对它赋值可以改变当前的地址,此语句即将目标

文件的起始地址设成了__START_KERNEL,定义为CONFIG_PHYSICAL_START +0xffffffff80000000CONFIG_PHYSICAL_STARTmenuconfig时设定的内核代码开始物理地址(默认为0x1000000);

另外,-Ttext 命令行选项也可以指定代码段的起始地址;LD的命令行选项可以覆盖链接脚本文件中的等同功能命令。

 

更详细的GNU LD详细用法请自行阅读:

 

4 QEMU调试
 
4.1 QEMU上安装GRUB2

启动qemurootfs,将/dev/hdafdisk分区并格式成ext2ext3,并挂载到rootfs/boot/目录上:

1) grub_install/拷至/xlpang/中(安装时的放置路径需确保和编译时的路径一致;也可以

修改/xlpang/grub_install/sbin/ grub-install脚本文件开头的变量路径),执行:

/xlpang/grub_install/sbin/grub-install --no-floppy /dev/hda命令将grub2安装到/boot目录下(实际硬盘挂载后的/目录下),并将boot.img, diskboot.imgkernel.img写入其相应扇区中,其它文件安装在/boot/grub/目录下。

注意事项:

由于busybox不支持grep –qx选项,需要修改grub-install脚本,否则在执行过程中会报错                                 

修改前:

………

if [ "x${devabstraction_module}" = "x" ] ; then

    if [ x"${install_device}" != x ]; then

      if echo "${install_device}" | grep -qx "(.*)" ; then

        install_drive="${install_device}"

………

修改后:(蓝色部分是对grepqx函数的实现)

………

grepqx()

{

         local line="$1"

         local file="$2"

         local ret

         ret=$(sed -n "/^${line}$/p" ${file})

         if [[ -z ${ret} ]]

         then

                   return 1

         else

                   return 0

         fi

}

if [ "x${devabstraction_module}" = "x" ] ; then

    if [ x"${install_device}" != x ]; then

      if echo "${install_device}" | grepqx  "(.*)" ; then

        install_drive="${install_device}"

………

 

2) 生成/boot/grub/grub.cfg文件(在hda1挂载的根目录下放置bzImage内核映像):

编辑/xlpang/grub_install/etc/grub.d/40_custom文件,增加:

menuentry "linux_x86_64-2.6.21" {

set  root=(hd0,1)

linux  /bzImage ro

initrd  /initramfs

}

3) 执行:/home/grub-mkconfig -o /boot/grub/grub.cfg

注意事项:

如果“/”是挂载到ramdisk文件系统,/bootmount到第一块硬盘第一个分区(/dev/hda1),且grub是安装在/boot上,则grub-mkconfig执行时会报如下错误:

解决方法:

修改grub-mkconfig脚本,设置grub-probe --target=device的路径为/boot,即grub安装路径,如下所示:

修改前:

# Device containing our userland.  Typically used for root= parameter.

GRUB_DEVICE="`${grub_probe} --target=device /`" 

GRUB_DEVICE_UUID="`${grub_probe} --device ${GRUB_DEVICE} --target=fs_uuid 2> /dev/null`" || true

修改后:

# Device containing our userland.  Typically used for root= parameter.

GRUB_DEVICE="`${grub_probe} --target=device /boot`"

 
4.2 QEMU上调试GRUB2

1) 使用仿真硬盘启动并进入调试模式命令:

qemu-x86_64.exe –S -boot c -hda hda500M.qcow -net tap,ifname=CEEE_TAP -m 512 -smp 4

-S表示进入调试模式,-boot c表示从硬盘启动,后面紧跟创建的虚拟硬盘文件

2) 进入后按ctrl+alt+2进入调试界面,输入”gdbserver”qemu上启动调试服务器;按

ctrl+alt+1可返回qemu运行界面;

3) 使用gdb_linux_x86.exegdb_linux_x8664.exe进行相关调试

   a) 运行gdb_linux_x86.exe vmlinux,此处vmlinux为第二个vmlinux

   b) 连接qemugdbservertarget remote localhost:1234

连接成功后有类似如下打印:

            (gdb) target remote localhost:1234

Remote debugging using localhost:1234

[New Thread 1]

warning: shared library handler failed to enable breakpoint

0x00000001 in ?? ()

   c)后续就类似于标准gdb调试应用程序的流程了,附几个常用命令:

      b *0x100000  /* 在0x100000处设置断点 */

      x/32i 0x1000000 /* 查看0x1000000开始处的指令 */

      info reg        /* 查看寄存器 */

      info b          /* 查看断点 */

      d           /* 删除第n个断点 */

      stepi          /* 汇编级单步调试 */

      c             /* 程序继续运行 */

 

4.3 QEMU上输出调试信息

qemu-x86/qemu_x86_64在启动过程中注册了一组特殊的io端口用来显示bochs bios的调试输出。我们将这组特殊的io端口称为boch调试端口。qemu-x86的启动函数pc_init1()中调用了boch端口注册函数bochs_bios_init完成调试端口的注册过程;写入调试端口0x402,0x403的数据将被打印出来,因此在内核启动初期可以采用向端口0x402或者ox403端口写入数据来完成信息的打印。

由于x86_64qemugdb调试有问题,可以通过添加打印来查看某此信息。

 

字符打印函数

可以采用如下x86汇编函数完成一个字符的打印。要求输入参数存储在eax寄存器中。

################################

#       function print char

################################

outchar:

#pramater al

         push %dx

         movw       $0x402,%dx

         outb %al,%dx

         pop  %dx

         ret

数字打印函数

8字节打印函数

打印一个8bit数,输入参数放置到寄存器al

 

outnum8:

#pramater %al

         push %bx

         push %cx

         push %dx

         movb        %al,%bl

         movw       $0x402,%dx

         mov %bl,%cl

         #print highest half byte    

         shrb  $4, %bl

         cmpb        $10,%bl

         movb        $48,%al

         jb      ship_add1

         addb         $7,%al

ship_add1:

         addb         %al,%bl

         movb        %bl,%al

         outb %al,%dx

         #print lower half byte

         movb        %cl,%bl

         andb         $0x0f,%bl

         cmpb        $10,%bl

         movb        $48,%al

         jb      ship_add2

         addb         $7,%al

ship_add2:

         addb         %al,%bl

         movb        %bl,%al

         outb %al,%dx

         pop  %dx

         pop  %cx

         pop  %bx

         ret    

16bit32bit数字打印函数

outnum16:

#pramater ax

         push %bx

         movw %ax,%bx

         shrw $8,%ax

         call   outnum8

         movw %bx,%ax

         andw $0xff,%ax

         call   outnum8

         pop  %bx

         ret

outnum32:

#pramater eax

         push %ebx

         mov %eax,%ebx

         shr    $16,%eax

         call   outnum16

         mov %ebx,%eax

call    outnum16

    pop %ebx

    ret

eip寄存器打印函数

#####################################################

#       function print eip

###################################################

prt_eip:

         movl (%esp), %eax

         call outnum32

         ret

例子

         我们可以通过以上定义的函数在内核启动初期Setup.S中打印一些信息。一下是一个使用实例:

打印esi的值

         #zhangxy printf esi

         push %eax

         push %esi

         mov %esi,%eax

         call   outnum32

pop %esi

pop %eax

 

setup.S代码中加打印eip的代码:

         #pangxunlei print ip

         push %ax

         call   prt_eip

pop %ax

 
 
5 问题分析

         有了以上对各流程的理解和准备,可以逐步有序开展问题定位工作。

 
5.1 核实内核解压

采用GRUB2启动X86_64内核失败,初步怀疑与重定向有关,由于LD链接时会对bzImage生成0x100000的符号地址。

 arch/x86_64/boot/Makefile中:$(obj)/bzImage: IMAGE_OFFSET := 0x100000

arch/x86_64/boot/ compressed /Makefile中:

LDFLAGS_vmlinux := -Ttext $(IMAGE_OFFSET) -e startup_32 -m elf_i386  /* 参考第3 */

因此第二个vmlinux会采用0x100000代码段起始地址(compressed中的head.Smisc.c)。

1) arch/x86_64/boot/compressed/head.S的执行入口startup_32CPU处于保护模式(未

开启分页);

2) 设置了段寄存器、堆栈和一些初始化后,执行decompress_kernel函数(位于misc.c);

   esi寄存器指向实模式下搜集的一些实模式数据起始物理内存地址;

3) decompress_kernel()检查是大内核后,进入gunzip解压代码,将内核解压至2个区域:

  a) low_buffer_endesi > 0x90000? 0x90000 : (unsigned int)esi) & ~0xfff;

区间1[0x2000, low_buffer_end)

区间2[0x1000000+ 区间的大小, 内核数据结束位置)

注:0x1000000为内核编译起始地址CONFIG_PHYSICAL_START

  b) 之后再进入head.S中,将区间1搬运到区间2的开头,形成0x1000000开头的完

整内核数据;

 

为排除解压过程发生错误,修改decompress_kernel()行为:直接解压至0x1000000处,并且head.S中不进入搬运流程,然后跳转至内核入口,仍然启动失败。

gdb查看0x1000000处的指令,如下:

1) 设置b decompress_kernel断点,显示在0x00105826内存地址处;说明bzImage

GRUB2放置在了0x100000处;显示0x1000000处的值为:“x1000000:      int3”,不是内核数据,因为还没完成解压;

2) x/32i 0x100000显示compressed/head.S在内存中的指令,并在call decompress_kernel

之后设置断点(保证内核解压完成),重新执行qemu

3) 断住后,再次查看内核解压数据,x/10i  0x1000000

  (gdb) x/3i 0x1000000

0x1000000:      mov    $0x18,%eax

0x1000005:      movl   %eax,%ds

0x1000007:      lgdtl  0x1000f00

 

结论:arch/x86_64/kernel/head.S开头的相符,至此可以表明内核解压缩正常,排

bzImage加载位置不对以及内核解压部分的因素。

 

5.2 分析compressed/head.S

精简compressed/head.S代码如下,修改解压代码如下(执行环境:32位保护模式,未开启分页):

.code32

    .globl startup_32

 

startup_32:  /* 0x100000入口地址 */

    cld

    cli

    movl $(__KERNEL_DS),%eax

    movl %eax,%ds

    movl %eax,%es

    movl %eax,%fs

    movl %eax,%gs

 

    lss stack_start,%esp

    movl 0x000000,%ecx

xorl %eax,%eax

 

    ljmp $(__KERNEL_CS), $0x100000 /* 从头开始。用grub2加载会不停返回到grub2的加载界面;不用grub2加载就不会有问题,即会一直循环解压不退出 */

 

结论:ljmp远指针远跳转指令时,使用grub2会出问题,但一直理不清为什么会跳转回grub2

加载界面。

 

5.3 核实setup.S

         compressed/head.S分析不下去了,转战setup.S,呵呵!

 

5.3.1 Setup的作用

arch/x86_64/boot/setup.SbzImage中的第二个512B开始的代码,大小在bootsect中有记录,setup的功能如下:

1)      ACPI兼容的系统中,调用BIOS例程建立描述系统物理内存布局的表。在早期系统中,它调用BIOS例程返回系统可以的RAM大小;

2)  设置键盘的重复延迟和速率;

3)  初始化显卡;

4)   检测IBM MCA总线、PS/2鼠标设备、APM BIOS支持等;

5)  如果BIOS支持Enhanced Disk Drive Services (EDD),将调用正确的BIOS例程建立描述系统

可用硬盘的表;

6)   如果内核加载在低RAM地址0x00010000,则把它移动到0x00001000处;如果映像加载在高内存1MB位置,则不动;

7)   启动位于8042键盘控制器的A20 pin

8)   建立一个中断描述表IDT和全局描述表GDT表;

9)   如果有的话,重启FPU单元;

10)  对可编程中断控制器进行重新编程,屏蔽所以中断,级连PICIRQ2不需要;

11)  设置CR0状态寄存器的PE位使CPU从实模式切换到保护模式,PG位清0,禁止分页功能;

12)  跳转到startup_32()汇编函数, jmpi 0x100000, __BOOT_CS,终于进入内核解压head.S

 

5.3.2 setup的内容

setup二进制程序开头部分是Linux内核与bootloader的协议约定(linux2.6.21用的是protocol 2.04),请参考documentation/i386/boot.txt的“THE REAL-MODE KERNEL HEADER”。

GRUB2loader/i386/pc/linux.cgrub_cmd_linux()函数中处理此实模式头部信息(位于bzImage0x200开头的512B内容中)的解析,根据这些信息来加载bzImage至相应的位置,GRUB2关于这些字段的信息位于include/grub/i386/linux.h中,其中:

#define GRUB_LINUX_BZIMAGE_ADDR     0x100000”定义了第二个vmlinux.bin文件被放置的内存地址,与5.1节中的分析一致,也即bzImage的解压部分被放置在0x100000处。

 

5.3.3 GRUB2加载setup的位置

qemu上,正常流程(不使用GRUB2加载)setup.S被放置在0x10200处(可用qemu的调试功能验证)。

qemu上,使用GRUB2加载bzImage,在0x10200处设置断点,发现断点不命中。

         结论:qemu上,GRUB2bzImage的加载流程发生了改变。

 

5.3.4 关键的一步

         最终在setup.S中,增加了打印地址的代码(参考4.3节,关于qemu的特色打印功能)想看看setup.S被加载到了哪个位置,发现打印不出来,比较奇怪,于是干脆在setup.S的入口处增加了死循环,结果GRUB2加载却没有死循环,仍然不停的异常重启。

         结论:GRUB2没有加载setup

 

5.4问题定位

         GRUB加载的时候已经处于保护模式,正常的流程是:加载bzImage->切至实模式->跳至setup执行->切至保护模式->跳至compressed/head.S中执行。

         GRUB2则是加载完bzImage后,完成了一些setup.S的工作后(如生成实模式数据,并设置esi),从保护模式直接跳转至了compressed/head.S中。

GRUB2加载linux-2.6.21 X86_64时不停的异常重启GRUB2,是在远跳转指令发生的,远跳转指令会更改CS寄存器,由于是保护模式会用到GDT,应该是由于没有正确设置GDT而造成的,只要正确设置GDT,而不用GRUB2的时候会跳转至setup.S中设置GDT,因此,我们相信只要将setup.S中的GDT移至compressed/head.S中,就可以完成GRUB2的正确加载,又由于是重复setup.S中的设置,不会影响不用GRUB2时的加载流程。

 

 

6 问题解决
 
6.1 方法一:再次设置GDT

compressed/head.S最后添加:

.data

pxl_gdt:

    .word   0, 0, 0, 0          # dummy

 

    .word   0, 0, 0, 0          # unused

 

    .word   0xFFFF              # 4Gb - (0x100000*0x1000 = 4Gb)

    .word   0               # base address = 0

    .word   0x9A00              # code read/exec

    .word   0x00CF              # granularity = 4096, 386

                        #  (+5th nibble of limit)

 

    .word   0xFFFF              # 4Gb - (0x100000*0x1000 = 4Gb)

    .word   0               # base address = 0

    .word   0x9200              # data read/write

    .word   0x00CF              # granularity = 4096, 386

                        #  (+5th nibble of limit)

    .align 256

pxl_gdt_48:

    .word   31           # gdt limit

    .long   pxl_gdt       # gdt base

 

 

compressed/head.S开始处添加:

startup_32:

    cld

    cli

 

    movl $(__KERNEL_DS),%eax

    movl %eax,%ds

    movl %eax,%es

    movl %eax,%fs

    movl %eax,%gs

lss stack_start,%esp

lgdt pxl_gdt_48

 

 
 
6.2 方法二:避免远跳转
6.2.1 AMD64架构简介

AMD64架构设计原因:无缝兼容legacy x86,更大的寻址空间和高性能(尤其是大块数据操作)。表现为更多的寄存器,64bit的寄存器,64bit的虚拟地址和物理地址。

AMD64内存模型兼容传统的(Legacy)X86内存模型,系统软件通过AMD64内存模型可以安全地管理用户程序和数据。类似于X86AMD64设计了硬件转换机制将虚拟地址映射到物理地址,这样就能保证系统软件透明地重定向物理内存空间(或操作系统管理的硬盘驱动器)的应用程序和数据。

         AMD64包括2种运行模式:长模式和传统模式。长模式的64-bit子模式实现了平坦内存模型(flat-memory model),传统模式实现了X86架构的所有内存模型。

X86支持4种运行模式,不同模式下提供了不同的内存管理、虚拟内存大小、物理内存大小,以及保护;4种模式为:

Ø  实模式(Real Mode)

Ø  保护模式(Protected Mode)

Ø  8086模式(Virtual-8086 Mode)

Ø  系统管理模式(System Management Mode)

   

AMD64支持X86的所有运行模式,称为“传统模式”(Legacy Mode);并设计了一种全

新的模式,称为“长模式”(Long Mode);因此,AMD64共有2种模式:传统模式和长模式;传统模式包括了X864种子模式,长模式包括64-bit模式和兼容模式。

软件可以实现在这些模式之间的切换,如下图所示。

6.2.1.1 长模式

         长模式包含2个子模式:64-bit模式(64-bit模式)和兼容模式(Compatible Mode)64-bit模式支持一些新特性,如64-bit虚拟地址空间;兼容模式允许在64位的系统软件(64位操作系统)下,运行16/32位应用程序(是二进制兼容的,不需要重新编译);长模式下取消了硬件多任务机制。

         声明:使用“长模式”描述时表示64-bit模式和兼容模式共有的特性,当子模式特有的功能时,会直接使用子模式的名称进行描述。

         使能和激活长模式(通过改变EFER.LME位,EFER:Extended Feature Enable Register)之前,系统软件必须先使能操作模式(即只能从保护模式进入长模式)

 

AMD64引入了长模式和它的2个子模式:64-bit模式和兼容模式。

         64-bit模式 完全支持64位的应用程序和系统软件,开启64-bit模式,需要64位的操作系统和64位的工具链支持。

         兼容模式 此模式实现了在64位的操作系统上兼容运行16/32X86架构的应用程序,这些应用程序不需要重新编译;

   

         长模式不支持如下2种模式:

Ø  8086模式 8086模式位(EFLAGS.VM)在长模式下被处理器忽略;当长模式使能时,试图开启虚8086模式将自动被处理器忽略,需要进入虚8086模式的系统软件必须先退出长模式。

Ø  实模式 进入长模式需要事先开启保护模式,所以长模式下不支持实模式。

传统X86模式

AMD64架构支持纯粹的传统X86模式,此模式二进制兼容16/32X86架构应用程序;更进一步地,与兼容模式不同的是,传统模式二进制兼容16/32位操作系统。传统模式支持实模式、保护模式和虚8086模式。复位时处理器总是处于传统模式(实模式)下,直到系统软件激活长模式。

6.2.1.2 64-bit模式

         64-bit模式支持64-bit的系统软件和应用程序,引入了如下新新性:

Ø  64-bit虚拟地址空间(具体处理器可能实现较小的空间)

Ø  通过REX指令前缀扩展寄存器:

-          增加R8-R15通用寄存器

-          所有通用寄存器扩充到64

-          增加了8YMM/XMM8-15 SSE 256/ 128位寄存器

Ø  64RIP寄存器

Ø  增加RIP相对数据寻址

Ø  平坦段地址空间

 

64-bit模式通过CS寄存器使能(控制64-bit模式和兼容模式之间的切换),传统的段机制

被禁用,分页机制需要使能,软件方面需要64位的系统软件和工具来运行此模式。

6.2.1.3 兼容模式

         对现有的X86下的16/32位应用程序保持二进制兼容,此模式下运行有长模式下的64位系统软件。

         兼容模式下,应用程序只能够访问4GB虚拟地址空间;CS寄存器控制兼容模式和64-bit模式之间的切换;与64-bit模式另一个不同,段机制和X86的保护模式保持一致,但与X86不同的是,分页机制是使能的。

         从应用程序的角度来看,兼容模式和开启了分页机制的X86保护模式是一致的;从系统软件(操作系统)的角度来看,兼容模式下运行的是长模式的系统软件、地址转换机制、中断异常处理,和系统数据结构。

6.2.1.4 传统模式

         AMD64的传统模式包括:实模式、保护模式和虚8086模式。传统模式下不仅能提供与X86架构的16/32位应用程序兼容,还能保持16/32位系统软件的兼容(相比,兼容模式下的系统软件必须是64位的)

         实模式

         又称为实地址模式。此模式下,处理器支持1MB的物理内存空间,操作数默认是16位,AMD64的实模式可能通过指令前缀(操作数大小指令前缀,66h)变成32位的;中断处理和地址生成和80286处理机保持一致;不支持分页;所有软件在特权等级0下运行。

         上电或重启时,处理器会运行在实模式下;长模式下没有实模式,原因是长模式需要从开启分页的保护模式切入。

 

         保护模式

         保护模式支持4GB的虚拟内存空间,4GB的物理内存空间,16位或32位操作数;所有的段式转换,段保护和硬件多任务机制都是可用的;分页是可选的;系统软件可以通过分段在虚拟地址空间中重定向有效地址。

         保护模式下,软件可以在特权等级0~3运行;通常应用程序运行在特权等级3,系统软件运行在特权等级0180286处理器首先引入了16位保护模式特性。

         8086模式

         通过保护模式切入;虚8086模式下,可以运行16位的实模式下运行的软件,在8086, 8088, 8018680188处理上运行的程序都可以在此模式下运行,但运行特权等级为3;支持虚拟地址空间1MB,操作数大小为16位,AMD648086模式下可以通过操作数大小指令前缀来使用32位的操作数;使用实地址模式转换;与实模式不同的是,分页是可选的,并且开启分页后的物理地址是32位的。

         EFLAGS.VM位控制虚8086模式的切入切出,POPF指令不能改变VM位,VM位只能通过2种方式访问:

         a) 任务切换时,从TSS段中载入EFLAGS

b) 特权软件执行IRET指令(中断发生时,硬件会自动将EFLAGS压栈)

        

长模式不支持虚8086模式,当长模式使能时,岂图使能虚8086模式将自动被硬件忽略。

6.2.1.5长模式下的代码/数据段

AMD64支持X86支持的所有形式的段,然而大部分现在系统软件并没有利用段来隔离任务,取而代之的是分页机制。因此AMD64长模式中的64-bit()模式取消了段的设计,使用了平坦内存模型,这使得新的64-bit系统软件的设计变得更加简单和高效。

 

长模式的地址转换过程如下图所示:

传统模式下的代码/数据段描述符如下:

 

 

长模式下的代码/数据段描述符如下:

      

  

图中灰色的部分表示忽略,长模式下忽略了段机制,只保留了几个字段用于控制作用。

抛开灰色字段被忽略的行为,X86AMD64两种架构的数据段字段定义是一样的,因此只要不发生远跳转,CPU就可以正常的执行指令。

 

6.2.2 避免远跳转

         注意到arch/x86_64/kernel/head.S中的GDT表(cpu_gdt_table)的数据段描述符的值和setup.S中设置的是一样的,第一个远跳转指令为进入长模式的指令(已经进入了长模式):

ljmp    $__KERNEL_CS, $(startup_64 - __START_KERNEL_map),因此,可以做如下修改:

    a) compressed/head.S中,将文件中的3处远跳转指令替换为近跳转指令,示例:

ljmp  $(__KERNEL_CS), $__PHYSICAL_START

修改为 ->

              movl  $__PHYSICAL_START, %ebp

              jmp  *%ebp

b) arch/x86_64/kernel/head.S中,将

      movl  $__KERNEL_DS,%eax

      movl  %eax,%ds

      lgdt   pGDT32 - __START_KERNEL_map

修改为 ->

lgdt   pGDT32 - __START_KERNEL_map

       movl  $__KERNEL_DS,%eax

       movl  %eax,%ds

   c) 针对SMParch/x86_64/kernel/trampoline.S中,跳至arch/x86_64/kernel/head.S前即

                ljmpl   $__KERNEL32_CS, $(startup_32-__START_KERNEL_map)

增加 ->

  mov $__KERNEL_DS, %ax

                mov %ax, %ds

 

阅读(665) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~