前段时间有个项目,要求在Linux下不经过BIOS重启,i386平台。

一、可行性分析

  众所周知,BIOS中包含了CPU及其他各种设备的初始化代码,Linux系统运行之后是否能够将各种用到的设备返回到刚被BIOS初始化后的状态是是否可行的关键。

  从项目的条件来看,外设并不是问题。因为要首先开起来的那个Linux只会用到磁盘系统。而通用的磁盘系统是不存在与启动相关的关键状态的。

  另外就是核心系统(CPU、内存初始化数据分布等)。CPU的状态时可以设置的,因此问题貌似也不大,将CPU返回实模式即可。内存中的BIOS数据也不会被Linux改动,因此也不会有问题。

二、Linux如何重启x86系统

  查阅Linux内核(2.6.33)中i386的关机代码(arch/x86/kernel/reboot.c),该文件与重启相关的关键点有三个(按代码的先后顺序):第一个是static int __init reboot_setup(char *str)函数和__setup("reboot=", reboot_setup);宏,这是在Linux启动时通过内核参数reboot=设置启动方式,记录到reboot_type变量中,默认为BOOT_KBD,即键盘启动。第二个是从static const unsigned long long real_mode_gdt_entries [3] = ... 一直到 void machine_real_restart(const unsigned char *code, int length)函数,这是专门为x86系统设计的不通过电源系统快速重启(直接跳到BIOS中重启)。第三个是static void native_machine_emergency_restart(void)函数,这是关机重启的最后阶段,且与前面的reboot_type呼应。要注意的是系统执行到这儿已经关闭了诸如中断控制器、重置了时钟、关闭了所有AP并确保接下来的代码都在唯一的BSP上执行。

  首先将第一和第三点。第一点是启动时的reboot_type设置,它影响到了第三点中实际restart操作的行为。native_machine_emergency_restart函数是重启过程中最后的步骤,i386系统重启最后都会走到这里来。这个函数的结构是一个死循环中包含一个switch(reboot_type)分支结构,如果reboot_type选定的那种重启方式执行失败了(正常情况下,这里调用的函数如果成功就不会返回了,直接导致系统重启。如果失败就会返回),那么就把reboot_type设置为默认的BOOT_KBD,再来重启一次。

  键盘方式看来是最稳妥最原始的重启方式,它的步骤是这样的:

for (i = 0; i < 10; i++) {
kb_wait();
udelay(50);
outb(0xfe, 0x64); /* pulse reset low */
udelay(50);
}

0x64端口是i8042键盘控制器的控制端口,0xfe命令字的意思是将P32-P21三个针脚拉为低电平,持续6usec。这段代码的实际效果就相当于你按下机箱上的 RESET 键。

  在那些重启方式中,还有一种方式是BOOT_BIOS,调用的就是第二个关键点中的machine_real_restart函数,它将CPU返回到实模式,然后跳到CPU上电后的那个地址(FFFF:0000),BIOS会在这个地址处放一个jump,跳到BIOS真正的开始处。

  显然我就可以直接拿这个函数开刀,把它改造成项目所需要的样子,如此一来,省去了再去写代码进行实模式切换的麻烦,直接用现成的。

三、分析和改造machine_real_restart函数

  为了尽量少更改原有的代码,另开了一个文件,将machine_real_restart函数和相关的结构体拷贝过来,然后慢慢改。当然,这个reboot文件也是要改的,就是再增加一种启动方式,我将它命名为BOOT_MBR,在第一点和第三点相关的地方增加一种启动方式即可。

  接下来就是着手分析改造了。首先看这部分功能包含哪些东西:

static const unsigned long long
real_mode_gdt_entries [3] =
{
0x0000000000000000ULL, /* Null descriptor */
0x00009b000000ffffULL, /* 16-bit real-mode 64k code at 0x00000000 */
0x000093000100ffffULL /* 16-bit real-mode 64k data at 0x00000100 */
};
static const struct desc_ptr
real_mode_gdt = { sizeof (real_mode_gdt_entries) - 1, (long)real_mode_gdt_entries },
real_mode_idt = { 0x3ff, 0 };
static const unsigned char real_mode_switch [] =
{
0x66, 0x0f, 0x20, 0xc0, /* movl %cr0,%eax */
0x66, 0x83, 0xe0, 0x11, /* andl $0x00000011,%eax */
0x66, 0x0d, 0x00, 0x00, 0x00, 0x60, /* orl $0x60000000,%eax */
0x66, 0x0f, 0x22, 0xc0, /* movl %eax,%cr0 */
0x66, 0x0f, 0x22, 0xd8, /* movl %eax,%cr3 */
0x66, 0x0f, 0x20, 0xc3, /* movl %cr0,%ebx */
0x66, 0x81, 0xe3, 0x00, 0x00, 0x00, 0x60, /* andl $0x60000000,%ebx */
0x74, 0x02, /* jz f */
0x0f, 0x09, /* wbinvd */
0x24, 0x10, /* f: andb $0x10,al */
0x66, 0x0f, 0x22, 0xc0 /* movl %eax,%cr0 */
};
static const unsigned char jump_to_bios [] =
{
0xea, 0x00, 0x00, 0xff, 0xff /* ljmp $0xffff,$0x0000 */
};
void machine_real_restart(const unsigned char *code, int length)
{
local_irq_disable(); spin_lock(&rtc_lock);
CMOS_WRITE(0x00, 0x8f);
spin_unlock(&rtc_lock); memcpy(swapper_pg_dir, swapper_pg_dir + KERNEL_PGD_BOUNDARY,
sizeof(swapper_pg_dir [0]) * KERNEL_PGD_PTRS); load_cr3(swapper_pg_dir); *((unsigned short *)0x472) = reboot_mode; memcpy((void *)(0x1000 - sizeof(real_mode_switch) - 100),
real_mode_switch, sizeof (real_mode_switch));
memcpy((void *)(0x1000 - 100), code, length); load_idt(&real_mode_idt);
load_gdt(&real_mode_gdt); __asm__ __volatile__ ("movl $0x0010,%%eax/n"
"/tmovl %%eax,%%ds/n"
"/tmovl %%eax,%%es/n"
"/tmovl %%eax,%%fs/n"
"/tmovl %%eax,%%gs/n"
"/tmovl %%eax,%%ss" : : : "eax"); __asm__ __volatile__ ("ljmp $0x0008,%0"
:
: "i" ((void *)(0x1000 - sizeof (real_mode_switch) - 100)));
}

首先是准备了与实模式对应的GDT表和IDT表。其中GDT表是为段寄存器设置的。段寄存器的可见部分是16为,还有48位不可见部分为对应的GDT表项,制定了段基址和段长度。machine_real_restart只是用了第三个,即基址为0x100,长度为64K的段,该entry偏移为0x10。

  接下来便是切换要用到的汇编代码real_mode_switch和jump_to_bios,这两段代码将会被拷贝并连接在一起,因此real_mode_switch最后一条指令movl  %eax,%cr0 执行完后,下一条指令就是jump_to_bios的ljmp...了。

  再接着就是主角登场。执行顺序是这样的:关中断=>通知CMOS将要重启=>load_cr3重置页目录,将内核3G-4G的映射改为0G-1G=> 通知BIOS进行热启动(跳过内存检查)=> 拷贝并拼接real_mode_switch和jump_to_bios的代码=> 加载实模式IDT和GDT => 根据新的GDT设置段寄存器 => 跳到real_mode_switch处

  最后阶段执行的代码是直接写的机器码。因为这段代码要拷贝到特殊位置0x1000附近,而编译器显然不会帮我们这么做。既然依靠不上编译器,那就只有自己写机器码了。

  要改造这个函数,首先去掉CMOS和BIOS相关的重启通知。因为我们要绕过BIOS重启,因此不要改变他们的状态。

  接下来就是要加入MBR的拷贝。为了简单起见,可以创建一个字符设备(命名为rebootmbr),在重启前将mbr拷贝到这个字符设备中(dd if=/dev/sda of=/dev/rebootmbr bs=512 count=1)。而该字符设备直接控制一块内存区。在重启的时候,将这块内存拷贝到0x7c00处,那个最后的ljmp也改为 0xea, 0x00, 0x00, 0xc0, 0x07 /* ljmp $0x07c0, $0x0000 */。

  最后就部署,然后重启吧!

四、问题和解决方法

  现实往往是残酷的。如果就像上面那样改动,结果往往是果真没有BIOS的画面了,而且可以重新进入GRUB然后看着Linux启动到图形界面,但是鼠标和键盘无法使用。而且无法启动Windows。

问题一:鼠标键盘无法使用

  导致该问题的原因是Linux在关机的最后会关闭所有可关闭的设备,包括鼠标键盘,因此Linux关机的最后阶段键盘是无法使用的。而显然GRUB不会帮你去打开它。因此得在重启的最后阶段把键盘打开。打开的方式是设置i8042。细节可查i8042相关的资料。

  即便是打开了i8042,仍然解决不了问题。这时我做了测试。自己写了一段MBR代码,然后重启进去。这段代码是死循环,永远读入键盘输入然后显示到屏幕上。其中读入键盘分为两种:从BIOS读和从i8042读。测试多遍发现:可以从i8042把刚输入的按键读出来,但是无法从BIOS服务读。原因就在于中断向量表!

  Linux启动时会设置一个叫做PIC的东西,对应的函数是init_8259A(int auto_eoi),里面有这样两段代码:

	/* ICW2: 8259A-1 IR0-7 mapped to 0x30-0x37 on x86-64, to 0x20-0x27 on i386 */
outb_pic(IRQ0_VECTOR, PIC_MASTER_IMR);
...
/* ICW2: 8259A-2 IR0-7 mapped to IRQ8_VECTOR */
outb_pic(IRQ8_VECTOR, PIC_SLAVE_IMR);

而实模式的要求是IRQ0-7映射到08-0f,IRQ8-15映射到70-77。Linux启动时将8259A(PIC)设置为了保护模式的状态,而在关机时仅仅将所有的中断源Mark掉了。所以还得自己改造一段init_8259A_real_mode()并在重启的时候调用。

  解决了鼠标键盘问题,就可以在GRUB中选择操作系统,并且在Linux启动的时候随意敲击键盘,然后看着启动日志排列成乱糟糟的模样。接下来如果细心些可以发现:Linux的时钟走的很慢。

问题二:计时有问题

  Linux支持多个时钟源(clock source),在多核或多cpu构架下,Linux一般会使用APIC内的时钟,此时它会将传统的8253/8254(PIT)关掉。从此系统不再计时。在单核单cpu构架下,Linux也会设置PIT为一种慢速状态,因此不经BIOS重启后,时钟会变慢。

  因此还得按照实模式下AT构架的计算机来设置PIT的0通道,使之产生一个约为18.3Hz的脉冲通向IRQ0。

  解决了这两个问题,估计我的Linux重启再也用不上BIOS了。但是,尝试着重启到Windows却出现问题了——启动时蓝屏,而且错误为IRQL_NOT_EQUAL_OR_LESS。为了解决这个问题,我翻阅了大部分Linux启动、关机相关的代码,查阅了Windows资料(微软这方面资料一般是说设备或驱动有问题,让你去找Manufacturer)。期间又发现了两个看似无关、其实相关的问题:无法重启进入带CDROM驱动的DOS,而且进入了不带CDROM的DOS后CPU速度相当慢。

问题三:不经BIOS重启后CPU速度超级慢

  我在重启后的DOS下用speed测速软件测,CPU的主频相当于只有60MHz左右,而真实情况是我的CPU是2GHz双核。翻阅了大量Intel的资料也没找出问题所在。而且发现重启后Linux解压内核的时间很长,一般是瞬间闪过的解压提示,要在那停留20多秒,但是过了那道坎Linux的速度就恢复了。

  后来一次偶然的机会,我将GRUB由0.4.2升级为0.4.4,居然可以启动Windows和带CDROM的DOS,而且CPU速度恢复了!比较了GRUB源码,发现了问题所在。

  原来那段real_mode_swich代码是这样来切入实模式的:andl  $0x00000011,%eax;orl   $0x60000000,%eax ;movl  %eax,%cr0,注释中也有类似的说明:“ Sets CD and NW, to disable the cache on a 486, and invalidates the cache.”。以往从来没注意,那条粗体指令居然禁用了CD和NW两个位,而这两位是用来控制缓存的。也就是说,缓存被禁用后,CPU读写内存数据是直接操纵内存,测得的60MHz是禁用了整台计算机的缓存系统后的核心系统速度。去掉这段黑体指令就万事OK了。

  GRUB0.4.2和Windows只开启PE和PG,而无视其他的位。Linux很聪明,将CD和NW都恢复成0了所以Linux基本不受影响,除了解压内核。

五、其他注意事项

  解决了上面几个问题后,基本上再没发现什么问题了。在解决问题的时候还补充了多个环节,比如关掉A20等。A20只影响实模式系统,运行于保护模式的操作系统都会在启动的时候把它打开。

from: http://blog.csdn.net/sonicling/article/details/5902182

Linux 下不经过BIOS重启(i386)的更多相关文章

  1. linux下如何启动/停止/重启mysql:

    一.启动方式1.使用linux命令service 启动:service mysqld start2.使用 mysqld 脚本启动:/etc/inint.d/mysqld start3.使用 safe_ ...

  2. Linux下Nginx安装/启动/重启/停止

    Nginx是高性能的web服务器也是非常好用反向代理服务器,可以实现负载均衡,动静分离等策略,在linux下用的非常多.下面是下载地址   http://nginx.org/en/download.h ...

  3. Linux下IP修改后重启服务器 oralce 出错(监听无法启动)

    针对linux下修改IP导致的Oracle不能启动问题的解决 主要修改/etc/hosts配置文件.修改前配置: # Do not remove the following line, or vari ...

  4. Linux下安装Oracle后重启无法登录数据库ORA-01034:ORACLE not available

    Linux下安装了数据库,安装完成后可以用,今天启动就不能用了,提示Oracle not available,后来查找资料,据说是oracle服务没有打开.如下方式可以解决问题. [root@root ...

  5. (七)Linux下的关机与重启命令

    ============================================================================================= 关机与重启命 ...

  6. linux 下apache 停止、重启等操作

    基本的操作方法:本文假设你的apahce安装目录为/usr/local/apache2,这些方法适合任何情况 apahce启动命令:推荐/usr/local/apache2/bin/apachectl ...

  7. linux下MySQL停止和重启

    一.启动方式1.使用linux命令service 启动:service mysqld start2.使用 mysqld 脚本启动:/etc/inint.d/mysqld start3.使用 safe_ ...

  8. Linux下Ngnix及PHP重启命令

    INT, TERM 立刻终止 QUIT 平滑终止 USR1 重新打开日志文件 USR2 平滑重载所有worker进程并重新载入配置和二进制模块 php-fpm 关闭: kill -INT `cat / ...

  9. Linux下mysql新建账号及权限设置各种方式总结

    来自:http://justcoding.iteye.com/blog/1941116 1.权限赋予 说明:mysql部署在服务器A上,内网上主机B通过客户端工具连接服务器A以进行数据库操作,需要服务 ...

随机推荐

  1. Extensions in UWP Community Toolkit - WebViewExtensions

    概述 UWP Community Toolkit Extensions 中有一个为 WebView 提供的扩展 - WebViewExtensions,本篇我们结合代码详细讲解 WebView Ext ...

  2. 换个视角来看git命令与代码库发生网络交互报错事件

    git的一系列命令中像 clone.pull.push等与代码库发生网络交互时,可能报下面的错误信息 fatal: remote error: CAPTCHA required Your Stash ...

  3. Spring之事务管理

        事务管理对于企业应用至关重要.它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性.     就像银行的自助取款机,通常都能正常为客户服务,但是也难免遇到 ...

  4. windows下 在cmd中输入ls命令出现“ls不是内部或外部命令“解决方法

    1.新建一个文件命名为 ls.bat 2.打开编辑这个文件 输入: @echo off dir 3.将这个文件放在C:\windows目录下

  5. 洛谷 P2590 [ZJOI2008]树的统计(树链剖分)

    题目描述一棵树上有n个节点,编号分别为1到n,每个节点都有一个权值w. 我们将以下面的形式来要求你对这棵树完成一些操作: I. CHANGE u t : 把结点u的权值改为t II. QMAX u v ...

  6. AutoCAD常用操作命令

    前言 最近工作需要使用AutoCAD画图,在这里记一下用到的一些常用操作,都是一些很基础的操作,希望对大家有帮助. 修剪 如果两条直线相交,你需要剪掉多余的部分,可以用修剪命令TR. 我们先画两条相交 ...

  7. 手动导入xmpp后,再使用cocoapods的时候出现的问题

    最新的cocoapod导入xmpp的时候,会出现循环依赖,所以撸主选择了手动导入. 一开始还用的挺开心的,后来,使用cocoapods导入其他的框架,发现调用的时候总是报错. Undefined sy ...

  8. Words to Use Instead of "Very"

  9. [HNOI 2002]彩票

    Description 某地发行一套彩票.彩票上写有1到M这M个自然数.彩民可以在这M个数中任意选取N个不同的数打圈.每个彩民只能买一张彩票,不同的彩民的彩票上的选择不同. 每次抽奖将抽出两个自然数X ...

  10. 【LA 3027 Corporative Network】

    ·一些很可爱的询问和修改,放松地去用并查集解决. ·英文题,述大意: 输入n(5<=n<=20000)表示树有n个节点,并且会EOF结束地读入不超过 20000个操作,一共有两种:    ...