linux boot-go_to_protected_mode

zeflove · · 3327 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

/*
 * Prepare the machine for transition to protected mode.
 * 从实模式向保护模式跳转
 * 文档:
 * /arch/x86/include/asm/segment.h 【1】
 * arch/x86/boot/pm.c【2】本文
 * arch/x86/boot/a20.c【3】
 */

#include "boot.h"
#include <asm/segment.h>

/*
* Invoke the realmode switch hook if present; otherwise
* disable all interrupts.
*
* boot loader hooks
* 当加载器运行的环境不能布置标准内存布局时,会用到加载器hooks。这种hooks尽量不要用。
* realmode_swtch:在转到保护模式前最后时刻执行,它是16位的(默认的例程是用来禁用NMI,
* 不可屏蔽中断)。
* code32_start : 这个字段可被用作hook,在转到保护模式后第一时刻执行,并且是32位的,
* 它的执行在内核解压前。实模式开始偏移512处是内核的开始。此时ds=es=ss=实模式代码加
* 载地址。推荐是fs=gs=ds=es=ss。一般情况下加载器会将它指向正常的、被压缩内核代码处。
*/
static inline void io_delay(void)
{	
	//boot.h:向0x80端口写al值来消耗时间
    const u16 DELAY_PORT = 0x80;
    asm volatile("outb %%al,%0" : : "dN" (DELAY_PORT));
}
static void realmode_switch_hook(void)
{
	/*
	 * 调用hook,一般hook是不会设置的,所以走else流程
	 * 向0x70商品写0x80关NMI
	 */
	if (boot_params.hdr.realmode_swtch) {
		asm volatile("lcallw *%0"
			: : "m" (boot_params.hdr.realmode_swtch)
			: "eax", "ebx", "ecx", "edx");
	} else {
		asm volatile("cli");
		outb(0x80, 0x70); /* Disable NMI */
		io_delay();
	}
}

/*
 * Disable all interrupts at the legacy PIC.
 * PIC相关
 * 端口范围:0x20-0x3f   8259  可编程中断控制器1
 * 端口范围:0xa0-0xbf   8259  可编程中断控制器2
 * 
 */
static void mask_all_interrupts(void)
{	
	outb(0xff, 0xa1);       /* Mask all interrupts on the secondary PIC */
	io_delay();
	outb(0xfb, 0x21);       /* Mask all but cascade on the primary PIC */
	io_delay();
}

/*
 * Reset IGNNE# if asserted in the FPU.
 * 端口范围:0x0f0-0x0ff 数学协处理器访问端口
 */
static void reset_coprocessor(void)
{
	outb(0, 0xf0);
	io_delay();
	outb(0, 0xf1);
	io_delay();
}

/*
* Set up the GDT
*/

struct gdt_ptr {
	u16 len;	// 0-15:GDT Lmit	 大小
	u32 ptr;	//16-47:Base address 基址
} __attribute__((packed));
/*
 * 启动时使用的gdt
 * 仅设置了3项cs,ds,tss,代码段cs和数据段ds的段基址都设置为0
 * 任务状态段段基址设置为4096
 * 由文档【1】中可知:  
 *	GDT_ENTRY_BOOT_CS=2
 *	GDT_ENTRY_BOOT_DS=3
 *	GDT_ENTRY_BOOT_TSS=4
 * 这些值表示在表中的索引,分别处于临时gdt的索引2、3、4项
 * 分别有__BOOT_CS=16,__BOOT_DS=24,__BOOT_TSS=32等
 * 段选择子与段描述符对应
 */
static void setup_gdt(void)
{
	/* There are machines which are known to not boot with the GDT
	being 8-byte unaligned.  Intel recommends 16 byte alignment. */
	static const u64 boot_gdt[] __attribute__((aligned(16))) = {
		/* CS: code, read/execute, 4 GB, base 0 */
		[GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),
		/* DS: data, read/write, 4 GB, base 0 */
		[GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),
		/* TSS: 32-bit tss, 104 bytes, base 4096 */
		/* We only have a TSS here to keep Intel VT happy;
		we don't actually use it for anything. */
		[GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),
	};
	/* Xen HVM incorrectly stores a pointer to the gdt_ptr, instead
	of the gdt_ptr contents.  Thus, make it static so it will
	stay in memory, at least long enough that we switch to the
	proper kernel GDT. */
	static struct gdt_ptr gdt;

	gdt.len = sizeof(boot_gdt)-1;
	gdt.ptr = (u32)&boot_gdt + (ds() << 4);
	//通过lgdtl将48位的gdt放入gdtr寄存器
	asm volatile("lgdtl %0" : : "m" (gdt));
}

/*
* Set up the IDT
* 中断描述符表初始化为0,先不使用
*/
static void setup_idt(void)
{
	static const struct gdt_ptr null_idt = {0, 0};
	asm volatile("lidtl %0" : : "m" (null_idt));
}

/*
 * Actual invocation sequence
 * 这个函数是从boot/main()跳过来执行的,它是pm.c中最主要的函数,它控制着保护模式
 * 相关函数的调用顺序。它的主要工作有:
 * 1.在离开实模式的最后时刻调用加载器hook
 * 2.打开A20Gate
 * 3.重置数学协处理器
 * 4.屏蔽所有中断
 * 5.设置idt
 * 6.设置gdt
 * 7.执行protected_mode_jump(code32_start hook)
 * 对于code32_start(document/x86/boot.txt),前面说过,它也能作为加载器hook,只是它是在进入保护模式第一时间
 * 执行的。但一般情况下,加载器会把它指向未解压的内核地址,hook比较少用。
 */
void go_to_protected_mode(void)
{
	/* Hook before leaving real mode, also disables interrupts */
	realmode_switch_hook();

	/* Enable the A20 gate */
	if (enable_a20()) {
		puts("A20 gate not responding, unable to boot...\n");
		die();
	}

	/* Reset coprocessor (IGNNE#) */
	reset_coprocessor();

	/* Mask all interrupts in the PIC */
	mask_all_interrupts();

	/* Actual transition to protected mode... */
	setup_idt();
	setup_gdt();
	/*
	 * boot_params在boot/main.c中定义
	 * ds从header.S到现在还没变,还是指向内核加载基址X所在的段。
	 * ds<<4+boot_params就是为boot_params找到实际的地址。
	 * 内核中的函数都是fastcall的,则参数1被放在eax中,参数2被放入edx中。
	 */
	protected_mode_jump(boot_params.hdr.code32_start,
		(u32)&boot_params + (ds() << 4));
}
////////////////////////////////a20.c//////////////////////////////////////////
/*
 * Enable A20 gate (return -1 on failure)
 * 在80286以前,intel的CPU只有20条地址线,可访问最高地址F000:FFFF=FFFFF,这种情况
 * 下20根地址线全1。80286时,有24条地址线,可访问FFFF:FFFF=10FFEFh地址(地址格式没
 * 变,只是范围大了),1M以上是extend memory,1M~10FFEFh为high memory。在80268以前
 * 如果给的地址大于F000:FFFF,如FFFF:FFFF=10FFEFh,那么因为地址线只有20根,高于20
 * 的地址部分将被弃掉,所以访问高于FFFF:FFFF实际上还是访问0FFEF。20根地址线最高
 * 是1M内存地址,所以要访问逻辑地址高于1M,都会访问1M以下的地址,这种情况被称作是
 * wraparound。而80286却能访问1M以上的地址。在80286实模式访问高端地址与8086不
 * 一致,这像是一个bug。所以为了它们一致,人们引入了A20Gate:利用键盘控制器8042空
 * 出的一个引脚和第20根地址线相与,以控制第20根。在80286实模式时,键盘控制器的这
 * 个引脚是0。
 * 注意一下,逻辑地址最高是FFFF:FFFF=FFFF0+FFFF=10FFEFh,在80286实模式下,就算访
 * 问这个地址,因为它被人为地置0了,就与8086一样了。最高4位,由于逻辑地址限制,最
 * 大值是0001,所以只要控制最高4位的的最低1位(A20)即可,因最高3位超出了逻辑地址
 * 表达的范围,它们永远是0。当然,80286以后的CPU也保留着实模式(与8088)的一致性,
 * 所以A20Gate一直存在。
 * 但对于80286及其以后的CPU,有了保护模式,寻址范围变大,如32位的CPU,20位以上的
 * 地址不再是仅有第20位是1、其它位是0了。而A20又成了遗留问题,进保护模式后,如果
 * A20还保持着实模式时的值---0,那么会导致一个地址范围找不到。所以在切换到实
 * 模式之间需要将A20Gate置为1。
 * A20打开的方法不限于8042一种,随着技术的发展,厂商设计出了若干种打开A20的方法
 * 下面会用到8042、0x92端口、bois(int15 ax=240x)
 */
  
#include "boot.h"

#define MAX_8042_LOOPS  100000
#define MAX_8042_FF     32


/*
 * 0x64:状态寄存器、命令寄存器
 * 0x60:数据寄存器
 * 如果要读8042状态,直接读0x64端口,如果要写则将命令写到0x64,将参数
 * 写到0x60,然后再读0x60端口取得返回值。
 * 0x60	R	读输出缓冲区
 * 0x60	W	写输入缓冲区(8042 Data&8048 Command)
 * 0x64	R	读状态寄存器
 * 0x64	W	写输入缓冲区(8042 Command)
 * 端口0x64读出的状态(8位): 
 * 0 0x60端口有数据,系统应该将它读出
 * 1 在输入寄存器中对8042有输入,来自0x60或0x64
 * 2 0:reset
 * 3 输入寄存器中的值是命令(1)还是数据(0)
 * 4 键盘使能(1)禁用(0)
 * 5 传输超时
 * 6 接收超时
 * 7 奇偶校验位 1=偶,0=奇,应该是奇
 */
static int empty_8042(void)
{
	u8 status;
	int loops = MAX_8042_LOOPS;
	int ffs   = MAX_8042_FF;

	while (loops--) {
		io_delay();

		status = inb(0x64);
		if (status == 0xff) {
			/* FF is a plausible, but very unlikely status */
			//返回ff说明8042控制器可能不存在
			if (!--ffs)
				return -1; /* Assume no KBC present */
		}
		/*
		 * 最低位是1说明60h端口有数据,将其读出		 
		 */
		if (status & 1) {
			/* Read and discard input data */
			io_delay();
			(void)inb(0x60);
		} else if (!(status & 2)) {
			/* Buffers empty, finished! */
			/*
			 * if句过滤最低位1,到此说明没有数据待读,最低位=0
			 * 此句又过滤掉bit1的1,那status最低2位应该是00
			 * 00表示0x60端口既没数据待读,系统也没往8042输入
			 * 数据或命令,8042empty了。
			 */
			return 0;
		}
	}

	return -1;
}

/* Returns nonzero if the A20 line is enabled.  The memory address
used as a test is the int $0x80 vector, which should be safe. */

#define A20_TEST_ADDR   (4*0x80)
#define A20_TEST_SHORT  32
#define A20_TEST_LONG   2097152 /* 2^21 */

static int a20_test(int loops)
{
	int ok = 0;
	int saved, ctr;

	set_fs(0x0000);
	set_gs(0xffff);
	/*
	 * fs:A20_TEST_ADDR		=00000+4*0x80	 =0000:0200
	 * gs:A20_TEST_ADDR+0x10=ffff0+4*0x80+0x10=0x100200
	 * wrfs32向fs写32位值,rdfs32从fs:addr处读出值
	 * wrfs32,rdgs32类似
	 *
	 * 上面说过80286实模式的wraparound现象,就是访问高端地址
	 * 时,由于A20的原因,最高位的1会被抛弃。那么,上面两个地
	 * 址,如果A20没打开的时候,读出的数据应该是相同的,异或时
	 * 结果等于0(ok=0)。对于下面的代码,分别从这两个地址读数
	 * 据,如果异或不是0,说明A20已经打开ok!=0,如果ok=0,说明
	 * A20没打开。当ok!=0时,会试loops次,确保不是偶然现象,代
	 * 码会要求loops的每一次都保证ok!=0(A20开启)。
	 */
	saved = ctr = rdfs32(A20_TEST_ADDR);

	while (loops--) {
		wrfs32(++ctr, A20_TEST_ADDR);
		io_delay();     /* Serialize and make delay constant */
		ok = rdgs32(A20_TEST_ADDR+0x10) ^ ctr;
		if (ok)
			break;
	}
	//将rdfs32处的值恢复原状
	wrfs32(saved, A20_TEST_ADDR);
	return ok;
}

/*
 * Quick test to see if A20 is already enabled 
 * 用一个较短的循环来探测
 */
static int a20_test_short(void)
{
	return a20_test(A20_TEST_SHORT);
}




/* 
 * Longer test that actually waits for A20 to come on line; this
 * is useful when dealing with the KBC or other slow external circuitry. 
 * 用一个较长的循环来探测,当用慢速设备(8042)打开A20时,需要用这种方法探测
 */
static int a20_test_long(void)
{
	return a20_test(A20_TEST_LONG);
}
/*
 * int15也可用来打开A20,
 * ax=0x2401 禁用
 * ax=0x2402 启用
 * ax=0x2403 查询A20状态
 * ax=0x2404 查询A20支持情况(8042还是0x92端口)
 */
static void enable_a20_bios(void)
{
	struct biosregs ireg;

	initregs(&ireg);
	ireg.ax = 0x2401;
	intcall(0x15, &ireg, NULL);
}

static void enable_a20_kbc(void)
{
	empty_8042();

	outb(0xd1, 0x64);       /* Command write写命令 */
	empty_8042();

	outb(0xdf, 0x60);       /* A20 on,在0x60端口写入命令值(开A20Gate)*/
	empty_8042();

	outb(0xff, 0x64);       /* Null command, but UHCI wants it */
	empty_8042();
}
/*
 * 0x92端口是快速的A20Gate,当系统没有键盘控制器时,就用这个端口
 * bit 1:1,enable; 0,disable
 * bit 0:1 reset;  0,no reset
 */
static void enable_a20_fast(void)
{
	u8 port_a;
	port_a = inb(0x92);     /* Configuration port A */
	port_a |=  0x02;        /* Enable A20  */
	port_a &= ~0x01;        /* Do not reset machine */
	outb(port_a, 0x92);
}

/*
* Actual routine to enable A20; return 0 on ok, -1 on failure
*/

#define A20_ENABLE_LOOPS 255    /* Number of times to try */

/*
 * 这个函数会用三种方法来尝试打开A20Gate有一个能打开就成
 */
int enable_a20(void)
{
	int loops = A20_ENABLE_LOOPS;
	int kbc_err;

	while (loops--) {
		/* First, check to see if A20 is already enabled
		(legacy free, etc.) */
		if (a20_test_short())
			return 0;

		/* Next, try the BIOS (INT 0x15, AX=0x2401) */
		enable_a20_bios();
		if (a20_test_short())
			return 0;

		/* Try enabling A20 through the keyboard controller */
		kbc_err = empty_8042();

		//这句测试是防止bios打开A20的延时
		if (a20_test_short())
			return 0; /* BIOS worked, but with delayed reaction */

		if (!kbc_err) {
			enable_a20_kbc();
			if (a20_test_long())
				return 0;
		}

		/* Finally, try enabling the "fast A20 gate" */
		enable_a20_fast();
		if (a20_test_long())
			return 0;
	}

	return -1;
}

//////////////////////////////////////////////////////////////////////////




有疑问加站长微信联系(非本文作者)

本文来自:CSDN博客

感谢作者:zeflove

查看原文:linux boot-go_to_protected_mode

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3327 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传