1.前言
在家无聊闲翻出来一个以前用过的华硕笔记本电脑:
-
型号:X552MJ2940-554LSF52X10(x64)
-
序列号:F7N0CV056919272
-
配置:
- BIOS(UEFI): American Megatrends Inc. X550MJ.201,2015/1/14
- 启动:\Device\HarddiskVolume1
- CPU(4核): Intel(R) Celeron(R) CPU N2940 @1.83Ghz
- 内存:8G
- 硬盘:kingston sa400s37480G
- 光驱:MATSHITA DVD-RAM UJ8G6
这个老家伙开机1分钟起步,电池似乎损坏了,只能插电使用,运行程序特别卡顿,我都想直接丢掉了,转念一想,这不正好让我有机会拿来试试看,能不能自己写个操作系统去用它,反正折腾坏了也不心疼,哈哈~
您各位可能会问了,Why u do that? Just for fun!
2.编写代码
说干就干,这个机器是BIOS(UEFI)模式启动的,直接制作UEFI启动太麻烦了,刚开始哪用搞这么复杂,我直接拿个U盘格式化了,将加载程序先写到它的MBR(主引导扇区),然后设置机器为U盘启动,加载MBR程序,先来看看可不可行,不过在继续之前,最好先把原机器的bios保存下来,网上找了个工具 pan.baidu.com/s/1FBFpPDoG… 来备份程序,使用很简单
先来一点前置补充,在存储介质中存在不同的分区,位于第一位置的即为MBR, 叫做主引导扇区,它是一个拥有512字节的区域,该区域内可包含:
- 446字节程序
- 64字节分区表
- 末尾2字节0x55aa魔数
知道了这些,那我们可以在446字节区域写程序,先来写一个最简单的helloworld:
[org 0x7c00]
; 设置文本模式:实模式
mov ax, 3
int 0x10 ; bios中断程序,用于设置文本显示模式
; 初始化段寄存器
mov ax, 0
mov si, booting
call print
; 阻塞
jmp $
; 定义字符串
booting:
db "hello,world!", 10, 13, 0
print:
mov ah, 0x0e
.next:
mov al, [si]
cmp al, 0
jz .done
int 0x10
inc si
jmp .next
.done
ret
; 将后续空间写为0
times 510 - ($ - $$ ) db 0
; 表示结束主引导扇区,硬性规定
db 0x55, 0xaa
这段汇编程序中先跳到内存0x7c00处,然后在实模式下利用bios提供的中断程序int 0x10来进行文本显示,后续空间全填0,末尾写0x55aa魔数
机器加电后bios程序就会自动读取u盘MBR的这段程序,然后跳到内存0x7c00处继续执行后面的内容,为什么会跳到这个地址呢?大家可以看看这篇 文章。
写好了汇编代码后需要把它编译成机器码,这时候就要用到:
- 编译器:
brew install nasm - 编译:
nasm -f bin -o boot.bin boot.asm
编译完成了就可以将其写入U盘了,在写之前需要先来处理一下:
- 插入U盘
- 列出usb设备:
diskutil list - 显示详细信息:
diskutil info /dev/disk3 - 格式化usb设备:
diskutil eraseDisk FAT32 USBDRIVE MBR /dev/disk3 - 查看分区:
sudo gpt -r show /dev/disk3 - 卸载usb设备:
diskutil unmountDisk /dev/disk3
处理好了以后就可以写入了:sudo dd if=boot.bin of=/dev/rdisk3 bs=1m count=1
为了避免出错重复拔插u盘,最好是先在模拟器上跑一跑,这里就要用到bochs: brew install bochs
它能模拟出一整套的各型号的元件供我们使用,首先要初始化:bochs后选择 "4. Save options to..." 输入配置名称 "bochsrc" , 它会生成默认的bochs配置,为了方便观察要修改:
- display_library: sdl2 图形化显示
- boot: disk 硬盘启动
- ata0-master: type=disk, path="master.img", mode=flat 指定镜像文件
# configuration file generated by Bochs
plugin_ctrl: usb_uhci=false, serial=true, speaker=true, unmapped=true, parallel=true, biosdev=true, iodebug=true, extfpuirq=true
config_interface: textconfig
memory: guest=32, host=32, block_size=128
romimage: file="/usr/local/Cellar/bochs/3.0/share/bochs/BIOS-bochs-latest", address=0x00000000, options=none, flash_data=none
vgaromimage: file="/usr/local/Cellar/bochs/3.0/share/bochs/VGABIOS-lgpl-latest.bin"
boot: disk
floppy_bootsig_check: disabled=0
floppya: type=1_44
# no floppyb
ata0: enabled=true, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, path="master.img", mode=flat
ata0-slave: type=none
ata1: enabled=true, ioaddr1=0x170, ioaddr2=0x370, irq=15
ata1-master: type=none
ata1-slave: type=none
ata2: enabled=false
ata3: enabled=false
optromimage1: file=none
optromimage2: file=none
optromimage3: file=none
optromimage4: file=none
optramimage1: file=none
optramimage2: file=none
optramimage3: file=none
optramimage4: file=none
pci: enabled=1, chipset=i440fx, slot1=none, slot2=none, slot3=none, slot4=none, slot5=none
vga: extension=vbe, update_freq=10, realtime=1, ddc=builtin, vbe_memsize=16
cpu: count=1, ips=4000000, model=corei7_haswell_4770, reset_on_triple_fault=1, cpuid_limit_winnt=0, ignore_bad_msrs=1, mwait_is_nop=0
print_timestamps: enabled=0
debugger_log: -
magic_break: enabled=1 0x0
port_e9_hack: enabled=false, all_rings=false
iodebug: all_rings=0
private_colormap: enabled=0
clock: sync=none, time0=local, rtc_sync=0
# no cmosimage
log: -
logprefix: %t%e%d
debug: action=ignore
info: action=report
error: action=report
panic: action=ask
keyboard: type=mf, serial_delay=150, paste_delay=100000, user_shortcut=none
mouse: type=ps2, enabled=false, toggle=ctrl+mbutton
com1: enabled=true, mode=null
com2: enabled=false
com3: enabled=false
com4: enabled=false
speaker: enabled=true, mode=gui
parport1: enabled=true, file=none
parport2: enabled=false
display_library: sdl2
这里需要一个img,所以还要去生成img镜像,这时会发现这些指令也太多了,放到makefile中更好处理:
# 定义汇编文件和目标文件
BOOT_ASM = boot.asm
BOOT_BIN = boot.bin
IMG = master.img
USB_DEVICE = /dev/rdisk3 # 替换为你的 USB 设备路径
# 指定编译器和模拟器
NASM = nasm
BOCHS = bochs
# 默认目标
all: $(IMG)
# 编译汇编代码
$(BOOT_BIN): $(BOOT_ASM)
@echo "Compiling $(BOOT_ASM)..."
$(NASM) -o $@ $<
# 创建磁盘映像
$(IMG): $(BOOT_BIN)
@echo "Creating $(IMG)..."
dd if=/dev/zero of=$(IMG) bs=1M count=1
dd if=$(BOOT_BIN) of=$(IMG) bs=512 count=1 conv=notrunc
# 将 bin 文件写入 USB MBR
usb: $(BOOT_BIN)
@echo "Writing $(BOOT_BIN) to USB MBR..."
sudo dd if=$(BOOT_BIN) of=$(USB_DEVICE) bs=512 count=1 conv=notrunc
# 运行 Bochs 模拟器
run: $(IMG)
@echo "Running Bochs..."
$(BOCHS) -q -unlock
# 清理生成的文件
clean:
rm -rf *.bin
rm -rf *.img
# 伪目标
.PHONY: all clean run usb
接着来试一下行不行:
- make clean
- make
- make run
ohhhh,we made it!
下一步就要把它做成u盘启动了:
- 格式化usb设备:
diskutil eraseDisk FAT32 USBDRIVE MBR /dev/disk3 - 卸载usb设备:
diskutil unmountDisk /dev/disk3 - 做成u盘启动:
make usb
# 将 bin 文件写入 USB MBR
usb: $(BOOT_BIN)
@echo "Writing $(BOOT_BIN) to USB MBR..."
sudo dd if=$(BOOT_BIN) of=$(USB_DEVICE) bs=512 count=1 conv=notrunc
3.实体机测验
前面已经将加载程序写进了u盘的主引导扇区,并且在bochs试过没问题,那现在就可以去实体机器上试试了
按下开机键后狂按F2, 就能进入华硕bios设置界面,前面说过,UEFI 模式下,系统通常不会从 MBR 启动,而是从 EFI 系统分区(ESP)中的 EFI 可执行文件启动,但我们只写入了MBR,所以需要开启Legacy Boot模式,这样才能从MBR启动,华硕笔记本启用Legacy Boot模式需要:
- Boot -> Launch CSM: "Enabled"
- Security boot -> "Disabled"
然后设置u盘启动后F10保存:
最终显示效果:
哈哈,迈出了一小步,搞懂了流程
4.解密bios
我们刚才知道了bios很关键,它会找到启动设备将程序加载到内存后执行,但它具体是怎么实现的呢?
光说不练假把式,所以还记得我们前面备份的bios.bin吗,是时候将其反编译看看里面究竟干什么了,先来安装idapro:mrx.hk/posts/f66fa… , 然后将其导入ida中,得到:
这个文件加载出来有26万行,主要关注*“引导程序”*的逻辑分析,但是要怎么找到呢?引导逻辑有什么特征呢?
可以发现这个文件里包括:
- BIOS_E:0000 到 BIOS_F:FFF8,16 位寻址模式,共 64KB 的地址空间,共14518行
- BIOS_FLASH:FFC00000 到 BIOS_FLASH:FFFFFFFF,32 位寻址模式,共 4GB 的地址空间,共250306行
在16 位寻址空间的末尾处,有如下的代码:
; 声明 start 是一个公共符号
BIOS_F:FFF0 public start
; proc near表示跳转时不会改变代码段寄存器 cs
BIOS_F:FFF0 start proc near
BIOS_F:FFF0
; 刷新 CPU 的写缓冲区,并使所有缓存无效
BIOS_F:FFF0 wbinvd
; 跳转到初始化代码
BIOS_F:FFF2 jmp loc_FF7F0
BIOS_F:FFF2 start endp
它是 BIOS 启动过程的入口点,将代码跳到loc_FF7F0执行,具体代码如下:
BIOS_F:F7F0 loc_FF7F0:
BIOS_F:F7F0 fninit ; 初始化浮点单元
BIOS_F:F7F2 movd mm0, eax
BIOS_F:F7F5 cli ; 关闭中断
BIOS_F:F7F6 xor eax, eax ; 设置段寄存器
BIOS_F:F7F9 mov es, ax
BIOS_F:F7FB assume es:BIOS_FLASH
BIOS_F:F7FB mov ax, cs
BIOS_F:F7FD mov ds, ax
BIOS_F:F7FF assume ds:BIOS_F
BIOS_F:F7FF mov ax, 0F000h
BIOS_F:F802 mov es, ax
BIOS_F:F804 assume es:BIOS_F
BIOS_F:F804 mov al, large byte ptr es:start ; 检查 BIOS 启动标志
BIOS_F:F80B cmp al, 0EAh
BIOS_F:F80D jnz short loc_FF81E
BIOS_F:F80F mov cx, 1Bh ; 检查 CPU 特性
BIOS_F:F812 rdmsr
BIOS_F:F814 test ah, 1
BIOS_F:F817 jz short loc_FF85A
BIOS_F:F819 jmp far ptr start
BIOS_F:F81E ; --------------------------发送 POST 代码-------------------------------------------------
BIOS_F:F81E
BIOS_F:F81E loc_FF81E: ; CODE XREF: BIOS_F:F80D↑j
BIOS_F:F81E mov al, 1
BIOS_F:F820 out 80h, al ; manufacture's diagnostic checkpoint (POST code)
BIOS_F:F822 mov esi, 0FFFFFF80h
BIOS_F:F828 db 66h
BIOS_F:F828 lgdt fword ptr cs:[si] ; 加载全局描述符表(GDT)的地址, 从代码段(cs)的 [si] 地址处加载一个 48 位的 GDT 指针
BIOS_F:F82D mov eax, cr0
BIOS_F:F830 or eax, 3
BIOS_F:F834 mov cr0, eax ; 启用保护模式
BIOS_F:F837 mov eax, cr4
BIOS_F:F83A or eax, 600h
BIOS_F:F840 mov cr4, eax
BIOS_F:F843 mov ax, 18h ; 设置数据段寄存器
BIOS_F:F846 mov ds, ax
BIOS_F:F848 assume ds:nothing
BIOS_F:F848 mov es, ax
BIOS_F:F84A assume es:nothing
BIOS_F:F84A mov fs, ax
BIOS_F:F84C assume fs:nothing
BIOS_F:F84C mov gs, ax
BIOS_F:F84E assume gs:nothing
BIOS_F:F84E mov ss, ax
BIOS_F:F850 assume ss:nothing
BIOS_F:F850 mov esi, 0FFFFFF86h
BIOS_F:F856 jmp large fword ptr cs:[si] ; 跳转到代码段的 0xFFFF FF86 地址处的远指针,继续执行后续代码。
BIOS_F:F85A ; ---------------------------------------------------------------------------
BIOS_F:F85A
BIOS_F:F85A loc_FF85A: ; CODE XREF: BIOS_F:F817↑j
BIOS_F:F85A ; BIOS_F:F85C↓j
BIOS_F:F85A cli
BIOS_F:F85B hlt
BIOS_F:F85C ; ---------------------------------------------------------------------------
BIOS_F:F85C jmp short loc_FF85A
简单分析后,可以很明显看到设置全局描述符表(GDT),并启用保护模式的内容,随后BIOS_F:F856 jmp large fword ptr cs:[si]跳转到代码段的 0xFFFFFF86 地址处的远指针,继续执行后续代码