极简Linux引导加载程序
Minimal Linux Bootloader (2018)

原始链接: https://raw.githubusercontent.com/Stefan20162016/linux-insides-code/master/bootloader.asm

这个为Linux设计的引导加载程序位于引导扇区(0x7c00)的前512字节中。它使用BIOS中断0x13/AH=0x42从硬盘读取扇区,在实模式下加载内核,初始地址为0x10000。然后,它从内核镜像中提取并打印内核版本字符串。引导加载程序设置内核设置头字段以启用堆使用并定义命令行参数,并将命令行复制到内存中。为了加载保护模式内核(可能大于64KB),它将内核分块读取到临时位置(0x20000),并使用中断0x15/AH=0x87和全局描述符表(GDT)将其复制到扩展内存,起始地址为0x100000 (1MB)。最后,在设置好段寄存器和堆栈指针后,它跳转到内核的实模式入口点(0x1020:0)以启动内核执行。错误处理包括在磁盘读取或内存复制失败时显示错误消息并重新启动。

这个Hacker News帖子讨论了一个“最小Linux引导加载程序”(2018年),重点在于其体积小巧以及与MBR/BIOS的兼容性。用户分享了相关的项目,例如sectorlisp和OSle,一位用户还提到在gokrazy中使用了它,并链接到一篇关于调试它的博文。讨论涉及到BIOS/MBR在现代系统中的局限性,一些人建议使用EFI直接引导Linux。其他人则指出像BIOS甚至IBM 360代码这样的遗留系统的持久性,突出了向后兼容性的负担和益处。用户们就其用途展开了辩论,一些人认为它更像是一个代码高尔夫练习,而不是像LILO这样的全功能引导加载程序的替代品。该引导加载程序的关键特性是其极小的体积(小于512字节),展示了引导加载程序创建的基本原理。

原文
; Minimal Linux Bootloader ; ======================== ; @ annotated version (added comments and the bootloader now prints the kernel version string which is in the kernel image file) Feb 2018, Stefan20162016 ; @ author: Sebastian Plotz ; @ version: 1.0 ; @ date: 24.07.2012 ; Copyright (C) 2012 Sebastian Plotz ; Minimal Linux Bootloader is free software: you can redistribute it and/or modify ; it under the terms of the GNU General Public License as published by ; the Free Software Foundation, either version 3 of the License, or ; (at your option) any later version. ; Minimal Linux Bootloader is distributed in the hope that it will be useful, ; but WITHOUT ANY WARRANTY; without even the implied warranty of ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ; GNU General Public License for more details. ; You should have received a copy of the GNU General Public License ; along with Minimal Linux Bootloader. If not, see . ; ubuntu kernel 4.13.13, qemu 2.8.0 ;- load first 512 bytes of kernel to 0x10000 using int 0x13/ah=0x42 ;- at 0x1f1 (497) size of kernel setup in 512 byte units, load it after first 512 bytes ;- check for boot protocol version in loaded kernel setup ;- set own kernel setup header fields: type of loader, can_use_heap, heap_end_ptr, cmd_line_ptr ;- move cmd_line to memory ;- read_protected_mode_kernel size according to syssize field in kernel_setup ;- use int 0x13/ah=0x42 to load 127*512=65024 byte chunks to temporary address 0x20000 ;- use int 0x15/ah=0x87 to copy it to extended memory using GDT global descriptor table: destination 0x100000 = 1 Mbyte ; Memory layout ; ============= ; 0x07c00 - 0x07dff Minimal Linux Bootloader max 446 bytes, ; ;first bootloader code than rest zeros til partition table ; ; + partition table = 4*16 bytes: 4 entries a 16 byte ; ; + MBR signature = 2 bytes for 0xaa55 ; for a total of 512 bytes ; ; 0x10000 - 0x17fff Real mode kernel ; size: 32Kb ; 0x18000 - 0x1dfff Stack and heap ; size: 24Kb ; 0x1e000 - 0x1ffff Kernel command line ; size: 8Kb ; total of 64Kb ; 0x20000 - 0x2fdff temporal space to load ; size: 63.5Kb is the max. size interrupt 15 in "do_move:" below can load at once: 127*512 bytes ; protected-mode kernel ; ; base_ptr = 0x10000 ; 64Kb; X in diagram in linux-insides linux-bootstrap-1.html respectively boot.txt in kerneldoc ; heap_end = 0x e000 ; 56Kb = sizeof(realmodekernel) + sizeof(stack and heap) ; heap_end_ptr = heap_end - 0x200 = 0xde00 ; minus sizeof(kernel boot sector) ; quote from kernel "boot.txt": "Set this field to the offset (from the beginning of the real-mode ; code) of the end of the setup stack/heap, minus 0x0200." ; cmd_line_ptr = base_ptr + heap_end = 0x1e000 ; ; a few comments ; use "objdump --adjust-vma 0x7c00 -z -bbinary -mi386 -D -Mintel,addr16,data16 MBR" to disassemble the MBR (-M are disassembler options) ; or use ndisasm -b16 -o7c00h -a -s7c4eh mbr (maybe change sync address or dont use the -s flag at all) ; double check size with: hexdump -C MBR (no bytes after 446) or else partition table will be corrupted ; I used a ubuntu 17.10 fresh install using VirtualBox for testing: compile kernel so it loads without initrd (check by deleting everything in the grub line except vmlinuz4.xx root=/dev/sda1) I just added config_sata_ahci=y to those options and config_pata_acpi ; qemu-img convert -O raw ubuntu1710.vdi ubu.raw ; check if it boots without initrd and get the lba# of the kernel with hdparm --fibmap to set "current_lba" at the bottom of the code; make sure hdparm gives you just one entry. If the kernel file is split over more lbas it wont work. Delete the file or copy it and check again. ; "dd if=mbr of=ubu.raw bs=1 count=446 conv=notrunc" don't forget the notrunc option or else the file ubu.raw will just be the mbr ; qemu-system-x86_64 -m 1024 -machine type=pc,accel=kvm -drive format=raw,file=ubu.raw ; qemu tipps: use ctrl+alt+f2 for the monitor console: ; e.g. "pmemsave addr size file" e.g. pmemsave 0 10485676 first1Mbyte ; you can check your current boot_params in /sys/kernel/boot_params/data (with hex editor) ; qemu option "-serial mon:stdio": if you compile the kernel with builtin-command-line-option: CONFIG_CMDLINE="console=ttyS0" you ; get all the output in your terminal instead of the seperate qemu window and of course the early-print kernel output with ; CONFIG_X86_VERBOSE_BOOTUP=y ; qemu-system-x86_64 -machine type=pc,accel=kvm -kernel arch/x86/boot/bzImage -initrd ~/code/kernel/rootfs.cpio.gz -serial mon:stdio -s -append "earlyprintk=ttyS0,keep,debug" ; qemu option "-snapshot" means changes to the file system won't be written ; get assembler/hardware documentation: "intel 64 and ia-32 architectures software developer's manual (combined volumes)" and http://ref.x86asm.net tables for quick reference and see http://www.ctyme.com/intr/int.htm for interrupts ; "This OS is a Boot Sector” by Shikhin Sethi" in "PoC || GTFO" series https://www.alchemistowl.org/pocorgtfo/pocorgtfo04.pdf or https://github.com/tylert/pocorgtfo/blob/gh-pages/pocorgtfo04.pdf org 0x7c00 ; 31Kb sets relative address because BIOS loads the bootloader at this address ; not enough bytes left for a proper boot-message-string, use kernel version string below ; mov si, boot_msg ;bootmsg_loop: ; lodsb ; and al, al ; jz done ; mov ah, 0xe ; mov bx, 7 ; int 0x10 ; jmp short bootmsg_loop ;done: ; just one char 'X' to see if we got loaded see below we print the kernel version string from insides the kernel image ; mov al,'X' ; mov ah,0xe ; mov bx,7 ; int 0x10 ; print X ; xor ax,ax ; int 0x16 ; press ENTER to boot cli xor ax, ax mov ds, ax mov ss, ax mov sp, 0x7c00 ; setup stack mov ax, 0x1000 mov es, ax sti read_kernel_bootsector: mov eax, 0x0001 ; register ax gets count: load one sector == 512 bytes xor bx, bx ; BX gets offset: no offset mov cx, 0x1000 ; CX destination segment(0x1000 shifted left 4bits): load Kernel boot sector at 0x10000, cx*16 segment=0x10000 call read_from_hdd ; first 512 bytes are now in memory at 0x10000 which is the kernel boot sector read_kernel_setup: xor eax, eax mov al, [es:0x1f1] ; no. of sectors to load; value is in 512 byte / 1 sector units; [es:0x1f1] also selects segment 0x10000 with offset 0x1f1 = 497 which are the bytes previously loaded (kernel boot sector) from the kernel file cmp ax, 0 ; if setup_sects == 0 set to 4. It's here for compatibility reasons jne read_kernel_setup.next mov ax, 4 .next: mov bx, 512 ; 512 byte offset, start after kernel boot sector mov cx, 0x1000 ; so same segment but we dont overwrite first 512 bytes call read_from_hdd ; next setup_sects * 512 bytes now after 0x10200 ; print kernel version push ds ; because we change it below mov esi, [es:0x20e] ; there is the pointer to the nul-terminated kernel version string add esi, 0x200 ; but minus 512 for kernel boot sector. See "header.S" in kernel source line ~ 310. mov ax, 0x1000 ; setup segment DS for lodsb below, see the (good) intel 64 and ia-32 architectures software developer's manual Chapter 3.2 and http://ref.x86asm.net tables for quick reference; LODSB loads from DS:(E)SI to AL mov ds, ax bootmsg_loop: lodsb and al, al jz doneB mov ah, 0xe mov bx, 7 int 0x10 jmp short bootmsg_loop doneB: pop ds ; restore DS segment register xor ax,ax int 0x16 ; press to boot ;check_version: ; skip those checks to have some bytes to print the kernel version string, a contemporary kernel has those flags set ; cmp word [es:0x206], 0x204 ; we need protocol version >= 2.04 ; es also selects segment 0x10000 ; jb error ; test byte [es:0x211], 1 ; check for LOADED_HIGH==1 test computes logical AND ; jz error ; jump if result 0 set_header_fields: mov byte [es:0x210], 0xe1 ; set type_of_loader or byte [es:0x211], 0x80 ; set CAN_USE_HEAP mov word [es:0x224], 0xde00 ; set heap_end_ptr ;mov byte [es:0x226], 0x00 ; set ext_loader_ver mov byte [es:0x227], 0x01 ; set ext_loader_type (bootloader id: 0x11) mov dword [es:0x228], 0x1e000 ; set cmd_line_ptr cld ; copy cmd_line mov si, cmd_line ; source DS:SI (intel manual vol2b chapter 4.3 MOVSB) mov di, 0xe000 ; destination ES:DI; so 0x1000:0xe000 == 0x1e000 mov cx, cmd_length rep movsb read_protected_mode_kernel: mov edx, [es:0x1f4] ; edx stores the number of bytes to load; 0x1f4 syssize in 16byte unit shl edx, 4 ; so shift left; now edx is in bytes .loop: cmp edx, 0 je run_kernel cmp edx, 0xfe00 ; less than 127*512 bytes remaining? jb read_protected_mode_kernel_2 mov eax, 0x7f ; load 127 sectors (maximum) xor bx, bx ; no offset mov cx, 0x2000 ; load temporary to 0x20000 call read_from_hdd mov cx, 0x7f00 ; move 65024 bytes (127*512 byte); the int15/ah=87h expects words (2 byte) in CX call do_move ; copy to extended memory/above 1Mbyte; see ralf browns Int table int15/ah-87h ; see GDTable below for source/destination address it loads from temporary 0x20000 ; to mem starting at 0x100000=1Mbyte sub edx, 0xfe00 ; update the number of bytes to load add word [gdt.dest], 0xfe00 ; add how much got loaded to "destination segment address": 0xfe00 = 127*512 = 65024 bytes so next loop will start there adc byte [gdt.dest+2], 0 ; add carry flag to dest+2 jmp short read_protected_mode_kernel.loop read_protected_mode_kernel_2: mov eax, edx ; edx in bytes shr eax, 9 ; eax in 512 byte sectors (divided by 2^9=512) test edx, 511 ; low 9bits == zero? jz read_protected_mode_kernel_2.next inc eax ; so add 1 one because SHR lost them .next: xor bx, bx ; offset 0 mov cx, 0x2000 ; this segment is the temp space call read_from_hdd mov ecx, edx shr ecx, 1 ; the int 0x15 ah=87 expects words (2 byte) in CX call do_move run_kernel: cli mov ax, 0x1000 mov ds, ax ; kernel will use that mov es, ax mov fs, ax mov gs, ax mov ss, ax mov sp, 0xe000 ;xor ax,ax ; (to stop before running kernelcode) ;int 0x16 ; press to boot jmp 0x1020:0; 0x10200: first kernel real-mode code (kernel setup) in 0x10000 + 512 byte = 0x10200 after kenel boot sector ; jump will set codesegment to 0x1020, CS can only be changed by another jump/ret in kernelcode ; see "retf" in header.S read_from_hdd: ; fill dap: disk address paket; set ax,bx,cx before calling push edx mov [dap.count], ax ; count mov [dap.offset], bx ; destination low part mov [dap.segment], cx ; destination high part mov edx, [current_lba] mov [dap.lba], edx ; source add [current_lba], eax ; update current_lba mov ah, 0x42 mov si, dap mov dl, 0x80 ; first hard disk int 0x13 jc error pop edx ret do_move: ; see http://www.ctyme.com/intr/rb-1527.htm push edx push es xor ax, ax mov es, ax mov ah, 0x87 ; SYSTEM - COPY EXTENDED MEMORY mov si, gdt int 0x15 jc error pop es pop edx ret error: mov si, error_msg msg_loop: lodsb and al, al jz reboot mov ah, 0xe mov bx, 7 int 0x10 jmp short msg_loop reboot: xor ax, ax int 0x16 int 0x19 jmp 0xf000:0xfff0 ; BIOS reset code ; Global Descriptor Table http://www.ctyme.com/intr/rb-1527.htm gdt: times 16 db 0 dw 0xffff ; segment limit .src: dw 0 ; first 2 bytes of source db 2 ; 3rd/last byte of source address, so 0x20000 = 128 KiB db 0x93 ; data access rights dw 0 dw 0xffff ; segment limit .dest: dw 0 db 0x10 ; load protected-mode kernel to 0x100 000 ; 0x10 is the highest byte of 3 see int15/ah=87 db 0x93 ; data access rights dw 0 times 16 db 0 ; 16 + 6*2 + 4 + 16 = 48 ; Disk Address Packet; that's what the BIOS int 13 expects dap: db 0x10 ; size of DAP db 0 ; unused .count: dw 0 ; number of sectors .offset: dw 0 ; destination: offset .segment: dw 0 ; destination: segment .lba: dd 0 ; low bytes of LBA address dd 0 ; high bytes of LBA address ;current_lba dd 39716864 ; initialize to first LBA address: hdparm --fibmap ;current_lba dd 43239424 current_lba dd 69445632 cmd_line db 'root=/dev/sda1 S', 0 ; S for single-mode runlevel/console, X11 works but takes longer to load with qemu cmd_length equ $ - cmd_line error_msg db 'err', 0xd,0xa,0 ; /* FIXME: newline */ ; "fixed"? ;boot_msg db 'Booting via minimal bootloader...', 0 ; not much space for a boot message use kernel version string instead times 510-($-$$) db 0 ; fill file with 0 until 510 dw 0xaa55 ; we dont necessarily need/write that just to test without partition table with qemu et.al. ; size of bootloader code has to end at 0x1BE or 446 bytes followed by the partition table than the two bytes for signature 0xaa55
联系我们 contact @ memedata.com