貌似简单的LDT:
老实说LDT除了有点绕以外,没有特别的,但是,就是这个简单的不能再简单的问题,折磨了我两天,所有的方法都用了,参考了intel保护模式手册,linux内核,minix内核,simplix操作系统,自己动手写操作系统,网络操作系统实验,把所有的无关的代码全去掉了,然后将汇编读了n遍,最后总是在使用ldt选择子时,操作系统当掉了,看着bios重新加载,简直欲哭无泪。最后找到一哥们一段不显眼的代码,发现原因了,终于我们的LDT成熟了,我们的LDT选择子,起作用了。以下尽量一一道来。
为什么要用LDT呢,实际上GDT只有一个,要实现进程和线程用LDT是最合适不过了,进程只要切换LLDT,线程只要切换段选择子是在合适不过了。
GDT和LDT很像,但说实在的真不是一个东西。
去掉浮云一样的多如牛毛的解释,gdt就是一个数组,lgdt就是gdt的指针,但就像c语言一样,数组需要基地址,元素大小,元素个数,访问时需要索引,gdt大小都是64位的,大小知道了,所以需要知道基地址和个数,所以实际上gdt存的是带大小的指针,而选择子就是索引,第一个保留,真正的第一个就是8,第二个就是16,第三个就是24,......,你仔细看就会发现最后三比特位0,然后是1,2,3
即1000b,10000b,11000b
ldtr就是索引,由ldtr和gdt(偏移和指针)定位到该段,找到该段基址,该段大小在界限里,这不是相当于gdt指针又定位到一个数组吗!所以其实两者很像!而索引也是如上所述定义,那么假如我jmp 4:1,你说我到底是去哪找呢,gdt数组还是ldt数组,所以后三位就存在着标志,gdt后三位为0,ldt后三位为4,其实都是浮云。所以ldt索引是0+4,8+4,16+4,当然ldt没有保留数组第一项,所以从0开始。
代码如下:
boot端
LoadSectorIndex equ 02
LoadSectorTotal equ 1
LoadAddress equ 7e00h
EntryPoint equ 0;12dh;00f8h;148h;
DA_C EQU 98h ; 存在的只执行代码段属性值
DA_LDT EQU 82h ; 局部描述符表段类型值
DA_32 EQU 4000h ; 32 位段
DA_DRW EQU 92h ; 存在的可读写数据段属性值
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限 1 (2 字节)
dw %1 & 0FFFFh ; 段基址 1 (2 字节)
db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
db (%1 >> 24) & 0FFh ; 段基址 3 (1 字节)
%endmacro ;
org 7c00h
mov ax,0
mov es,ax
mov bx,LoadAddress
mov ah,2
mov al,LoadSectorTotal ;加载扇区数
mov ch,0
mov cl,LoadSectorIndex
mov dh,0
mov dl,0
int 13h ;将磁盘第LoadSectorIndex扇区开始LoadSectorTotal个扇区装入内存LoadAddress
jmp start
;定义GDT
GDT_ADDRESS:
dw 0,0,0,0 ;默认
CS_ADDRESS:
dw 0xFFFF,0,0x9A00,0x00CF ;代码段,大小4G
dw 0xFFFF,0,0x9200,0x00CF ;数据段, 大小4g
LABEL_DESC_LDT: Descriptor 0, 7, DA_LDT ; LDT
GDT_LEN equ $-GDT_ADDRESS
;SelectorLDT equ LABEL_DESC_LDT - GDT_ADDRESS
;GDTR LOAD
GDT_LOAD:
dw GDT_LEN
dw 0,0 ;compute as follow
CS_SELECTOR equ CS_ADDRESS-GDT_ADDRESS
;IDTR LOAD 中断向量
IDT_LOAD:
dw 0
dw 0,0
start:
;设置GDT基址
xor eax,eax ;清零
;compute ds:GDT_ADDRESS
mov ax,ds
shl eax,4 ;段地址乘以16
add eax,GDT_ADDRESS
mov dword [GDT_LOAD+2],eax ;装入基地址
lgdt [GDT_LOAD] ;加载到GDTR寄存器
;初始化新的中断向量表
lidt [IDT_LOAD]
;屏蔽所有可屏蔽中断-关掉所有8259中断
mov al,0xff ;mask all interrupts for now
out 0xA1,al
out 0x80,al ;Delay is needed after doing I/O
; 保证所有的协处理都被正确的Reset
xor ax, ax
out 0xf0,al
out 0x80,al ;Delay is needed after doing I/O
out 0xf1,al
out 0x80,al ;Delay is needed after doing I/O
;进入32位模式 ;//CR0寄存器的PE位
mov ax,1
LMSW ax
;设置段为第二项-数据段
mov ax,0x10
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
mov ss,ax
mov esp,0x200000
mov ebp,0x200000
jmp CS_SELECTOR:kernel_start+EntryPoint
dw 0
times 510-($-$$) db 0 ; 填充剩下的空间,使生成的二进制代码恰好为512字节
dw 0xaa55 ; 结束标志
kernel_start: ;占位符,会放置c语言编译后的代码
db 0
代码没什么稀奇的,需要说明的是
LABEL_DESC_LDT: Descriptor 0, 7, DA_LDT ; LDT
加一个ldt代码段,从0开始,0(非段基址的0,而是首地址),1,2,3,4,5,6,7加起来正好是8个字节,
我们准备覆盖中断向量表,没啥的,我们不用现在。
好了c端代码,
typedef unsigned short uint16_t;
typedef unsigned char uint8_t;
#pragma pack(1)
struct Segment_Descriptor
{
uint16_t limit1;
uint16_t base1;
uint8_t base2;
uint16_t typelimit2;
uint8_t base3;
};
#define SA_TIL 4
#define lldt(ldtr) \
asm("lldt %0" : : "r" (ldtr))
#define DA_CR 0x9A
#define DA_C 0x98 //
#define DA_32 0x4000 // 32 位段
#define DA_DRW 0x92 // rw data section
void entry_os_start();
void entry_os()
{
entry_os_start();
}
#define BASE_ADDRERSS 0xb8000
#define LIMIT_ADDRESS 0x0ffff
#define DESCRIPTOR_TYPE (DA_C + DA_32)
void set_gssec()
{
uint16_t selector;
struct Segment_Descriptor* pLTDS=(struct Segment_Descriptor*)0;
pLTDS->limit1= LIMIT_ADDRESS & 0xffff;
pLTDS->base1 = BASE_ADDRERSS & 0xffff;
pLTDS->base2 = (BASE_ADDRERSS >> 16) & 0xff;
pLTDS->typelimit2 =((LIMIT_ADDRESS>>8)&0xf00)|(DESCRIPTOR_TYPE & 0xf0ff);
pLTDS->base3 = ((BASE_ADDRERSS) >> 24) & 0xff;
selector=sizeof(struct Segment_Descriptor);
selector=24;
lldt(selector);
asm("ljmp $4,$0" );
}
void entry_os_start()
{
set_gssec();
for(;;){}
}
这里去掉了显示环节,需要说明几点,
我们将entry_os移到开头,然后跳转到真正的entry,我们不想老去idapro哪里去找跳转地址,所以一般情况下直接跳转到头部就okay了,如果bochs显示的汇编不对,再修改不迟。
SA_TIL就是区别ldt和gdt标志
asm("lldt %0" : : "r" (ldtr))摘自minix源码,
即相当于(实际上gcc中间又多分配了一个局部变量)
mov ax,ldtr
lldt ax
注意这里r就是指任选一寄存器作为中间变量(由编译器选)
asm("ljmp $4,$0" );
即jmp 4:0
实际上cpu一看到这条指令,他就知道是ldt,因为gdt的索引尾巴都是000b,然后根据gdtr和ldtr得到具体地址。
最后就要指出花了两天得来的
#pragma pack(1)
本来没有这句的,然后装入bochs运行,总是错误,查了n遍intel手册也没有找到为什么会异常,终于找到一哥们这样写gdt
struct DESCRIPTOR //共 8 个字节
{
t_16 limit_low; //Limit 2字节
t_16 base_low; //Base 2字节
t_8 base_mid; //Base 1字节
t_8 attr1; //P(1) DPL(2) DT(1) TYPE(4) 1字节
t_8 limit_high_attr2; //G(1) D(1) 0(1) AVL(1) LimitHigh(4) 1字节
t_8 base_high; //Base 1字节
};
我就纳闷了,为什么这么写呢?百无聊赖间想会不会是对齐,
还真是!
uint16_t limit1;
uint16_t base1;
uint8_t base2;
uint16_t typelimit2;
uint8_t base3;
走到base2时,vc和gcc统统2字节对齐,谁让咱就开始没设好编译器呢,
selector=sizeof(struct Segment_Descriptor);
是后加的,结果显示为0xa,即10,肯定不对,应为8
所以加上了
#pragma pack(1)
告诉编译器struct一字节对齐,okay,跳了,终于,跳转了。
(0) [0x000b8000] 0004:0000000000000000 (unk. ctxt): push eax
跳到了段地址首地址上0x000b8000
未完待续