x86的段机制是实现程序的逻辑地址到线性地址的映射的一种机制。
我们先介绍实现这个机制的几个组成部件,它包括一些软件的东西和一些硬件的东西:硬件的东西有:段寄存器、分段部件、段描述符高速缓冲器;软件的东西有(即几个数据结构):段描述符表、段描述符;另外得知道的两个概念前面已经介绍过了:逻辑地址和线性地址。
下面我们看段机制是怎么用这些概念实现的。
由于段寄存器是16位的,所以它不能存储32位的基地址,但是这32位基地址又必须得存,于是我们把这32位基址存储到一块内存中,而把这个地址的索引放段寄存器里边,从而得到段基址,这是大体思路。但x86做得更好。
我们知道程序都有模块性,一个复杂的大程序总可以分解成多个在逻辑上相对独立的模块或者线程。每个模块都是一个单独的段,都以该段的起点为0相对编址。也就是说,我们得用一个数据结构描述各个段的相关信息,比如该段装进内存的基址(段基址),该段的长度,以及该段是否存在内存中等信息,这个数据结构就是段描述符。言下之意,我们每个模块都有一个段描述符,用来描述该段的属性,而段描述符表其实就是用来盛载这些段描述符的一个表格,一个段描述表可能长这样:
其中的表项就是段描述符,段描述要存储三个信息:基地址、段长度和段属性。我们用8个字节来存储这三个信息,首先我们说段基址是32位,所以基址占四个字节32位,然后我们用20位来描述段长度。那么这8个字节64位还剩下12位,就用来存储段的属性信息了。
G:一位,G=0:表示以字节为单位表示段的长度,刚才我们说了我们用20位存储段的长度,那么这样我们段的最大长度就是2^20次方,即1M大小,即我们给程序分段的时候,一个段最大是1M。G=1:表示以4KB表示段的长度,这样我们一个段的最大长度就是2^20次方乘以4KB,即4GB。后边我们会知道linux内核就让这个G等于1,从而直接越过x86的段机制,使逻辑地址没映射线性地址,再映射到物理地址,而是直接映射到物理地址(分页机制)。
D:一位,表示操作数的位数,D=1表示32位操作数,D=0表示16位操作数,这显然是为向下兼容而设计的。
接下来两位暂时没用,可留作扩展。
P:一位,表示这个段是否在内存中,如果我们要把这个段加载进内存,那么在加载进去的时候把这个值修改为1,否则让它等于0。
DPL:两位,表示段描述符的特权级。
S:一位,表示这个段是系统段还是用户段。S=0,则为系统段,即内核专门使用的段,S=1,表示用户段,即为程序的代码段、数据段或堆栈段。
类型占三位:依次是E、D、W。E=0,为数据段描述符,这时D位表示数据的扩展方向,D=0,表示向地址增大的方向扩展,反之,向地址减小的方向扩展。E=1时,直接表示的是数据段,此时W=0时表示数据段不能写(我们就可以想到C++中定义const变量的时候,最后肯定是修改了这个值的),W=1,数据段可写。
保护模式下,有三种类型的描述符表,分别是全局描述符表(GDT),中段描述符表(IDT),局部描述符表(LDT)。为了加快对这些表的访问,Inter设计了三个专门的寄存器,GDTR、IDTR、LDTT,以存放这些表的基地址和表的长度界限。
下来我们看段寄存器。我们说过段寄存器存放的就是段描述符在段描述符表中的索引,其实段寄存器还存了另外两个信息,这意味着,我们不能用着16位全部来存储索引值。其实Inter只用了13位来存储段描述符的索引,另外还用1位标志我们是从全局描述符表(GDT)中选择段描述符还是从局部描述符表(LDT)中选择段描述符。剩下两位表示请求者的特权级。
保护模式提供了4个特权级,用0-3表示。0表示最高特权级,对应内核态,此时它可以访问内核代码,也可以访问用户代码;3表示最低特权级,对应用户态,此时它只能访问用户态代码。
下面是段机制的硬件构成:
这样就完成了逻辑地址到线性地址的映射。
这节我们讨论linux是如何利用x86结构中的段机制的,更确切的说是如何绕过linux的段机制的。
我们决定从linux的可移植性开始讨论。我们说linux是一个广泛移植的操作移动,它支持x86,Alpha,arm等多种体系结构。但是很多的结构其实都是不支持段机制的,比如arm,Alpha等,但是他们都支持分页机制。linux为了能移植到x86上,做了不少工作。
首先我们说,x86是肯定有段机制的,那么我们要在x86上运行程序,那不可避免要用到段机制。于是我们想到我们先前所想到的段描述符中有一个表示以字节为单位还是以页为单位表示一个段长度的属性位。我们当时说,当G=1时表示以页(4KB)为单位,那么一个段最大长度能到4GB。根据这一点,我们把一个段的段基址固定设置为0,然后让G=1,于是我们一个段的最大长度就是4GB了,呐,这个很显然就能和我们4GB的线性地址空间一一映射了。通过这样的处理,我们说现在x86的段机制已经形同虚设了,逻辑地址和线性地址可以混为一谈了。
但是x86还规定说,必须为代码段和数据段创建不同的段,所以linux为代码段和数据段分别创建了一个基地址为0,段长度为4GB的段描述符。不仅如此,由于linux内核运行在特权级0,用户程序运行在特权级3,x86规定说特权级为3的用户程序是不能访问特权级为0的内核代码的,所以linux又分别为内核和用户程序分别创建代码段和数据段。
于是在arch/x86/include/asm/segment.h中这样定义四个段(即在机器启动过程中段寄存器中放的值):
#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS*8) #define __KERNEL_DS (GDT_ENTRY_KERNEL_DS*8) #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS*8+3) #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS*8+3)
其中:
#define GDT_ENTRY_DEFAULT_USER_CS 14 #define GDT_ENTRY_DEFAULT_USER_DS 15 #define GDT_ENTRY_KERNEL_BASE (12) #define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE+0) #define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE+1)
于是上边的定义的结果是下边这样:
#define __KERNEL_CS 0x00C0 /*内核代码段,index=12,TI=0,RPL=0*/ #define __KERNEL_DS 0x00D0 /*内核代码段,index=13,TI=0,RPL=0*/ #define __USER_DS 0x00E3 /*用户代码段,index=14,TI=0,RPL=3*/ #define __USER_CS 0x00F3 /*用户代码段,index=15,TI=0,RPL=3*/
于是我们可以用12,13,14,15四个索引来找到我们四个段所对应的段描述符,并且我们把内核代码段的特权声明为0,用户代码段的特权为3,TI为0表示我们总是访问全局描述符表。
在我们对应的段描述符中我们把G设置为1,段上限规定为0xfffff,就巧妙的绕过了x86的段机制。
但在这里我不能忽略的一个问题就是,我们把四个段的上限全部设置为4G,那就完全破坏了段的保护,就是说,我们有可能随随便便就修改了我们的其他段的数据。所幸,我们现在还是个线性地址,所幸此时我们还没把数据装载进内存,因此,我们就有处理这个问题的办法,这就是下面要讲的分页机制了。
一、编写源代码
源代码:
/*******************************led_off.S**************************/
.text
.global _start
_start:
LDR R0,=0x56000010
MOV R1,#0x00015400
STR R1,[R0]
LDR R0,=0x56000014
MOV R1,#0x0df
STR R1,[R0]
MAIN_LOOP:
B MAIN_LOOP
/*******************************Makefile****************************/
*指定链接文件地址
*指定链接文件顺序
*********************************************************************/
led_off.bin : led_off.S
arm-linux-gcc -g -c -o led_off.o led_off.S
arm-linux-ld -Ttext 0x0000000 -g led_off.o -o led_off_elf
arm-linux-objcopy -O binary -S led_off_elf led_off.bin
clean:
rm -f led_off.bin led_off_elf *.o
/*******************************************************************/
二、使用Jlink下载led_off.bin到nand flash(参考烧写Uboot方法)
5.1 打开 J-Link Commander,输入-r
5.2 speed 12000
5.3 J-Link Commonder 输入loadbin f:\init.bin 0
5.4 setpc 0
5.5 g
5.5 h
5.6 J-Link Commonder 输入loadbin f:\u-boot.bin_openjtag 0x33f80000
5.7 setpc 0x33f80000
5.8 g
5.9 h
5.10 J-Link Commonder 输入loadbin f:\u-boot.bin 0x30000000
******************************************************************************************
J-Link Commonder 输入loadbin f:\led_off.bin 0x30000000,即可以烧写汇编程序
******************************************************************************************
5.11 g
5.12 h
5.13 在secretcat 中输入nand scrub
5.14 y
5.15 在secretcat 中输入nand erase 0 0x40000
5.16 在secretcat 中输入nand write.jffs2 30000000 0 0x40000
5.17 重新启动进入nand flash
三、开发板重启即可。