assembly - 如何为我的 Bootstrap 制作内核?

标签 assembly x86 kernel bootloader osdev

我正在尝试制作自己的自定义操作系统,我需要一些代码方面的帮助。
这是我的bootloader.asm:

[ORG 0x7c00]

start:
    cli
    xor ax, ax
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov [BOOT_DRIVE], dl
    mov bp, 0x8000
    mov sp, bp
    mov bx, 0x9000
    mov dh, 5
    mov dl, [BOOT_DRIVE]
    call load_kernel
    call enable_A20
    call graphics_mode
    lgdt [gdtr]
    mov eax, cr0
    or al, 1
    mov cr0, eax
    jmp CODE_SEG:init_pm

[bits 32]
init_pm:
    mov ax, DATA_SEG
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov ebp, 0x90000
    mov esp, ebp

    jmp 0x9000

[BITS 16]
graphics_mode:
    mov ax, 0013h
    int 10h
    ret

load_kernel:
                        ; load DH sectors to ES:BX from drive DL
    push dx             ; Store DX on stack so later we can recall
                        ; how many sectors were request to be read ,
                        ; even if it is altered in the meantime
    mov ah , 0x02       ; BIOS read sector function
    mov al , dh         ; Read DH sectors
    mov ch , 0x00       ; Select cylinder 0
    mov dh , 0x00       ; Select head 0
    mov cl , 0x02       ; Start reading from second sector ( i.e.
                        ; after the boot sector )
    int 0x13            ; BIOS interrupt
    jc disk_error       ; Jump if error ( i.e. carry flag set )
    pop dx              ; Restore DX from the stack
    cmp dh , al         ; if AL ( sectors read ) != DH ( sectors expected )
    jne disk_error      ; display error message
    ret
disk_error :
    mov bx , ERROR_MSG
    call print_string
    hlt

[bits 32]
    ; prints a null - terminated string pointed to by EDX
print_string :
    pusha
    mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem.
print_string_loop :
    mov al , [ ebx ] ; Store the char at EBX in AL
    mov ah , WHITE_ON_BLACK ; Store the attributes in AH
    cmp al , 0 ; if (al == 0) , at end of string , so
    je print_string_done ; jump to done
    mov [edx] , ax ; Store char and attributes at current
        ; character cell.
    add ebx , 1 ; Increment EBX to the next char in string.
    add edx , 2 ; Move to next character cell in vid mem.
    jmp print_string_loop ; loop around to print the next char.
print_string_done :
    popa
    ret ; Return from the function

[bits 16]
; Variables 
ERROR_MSG db "Error!" , 0
BOOT_DRIVE: db 0
VIDEO_MEMORY equ 0xb8000
WHITE_ON_BLACK equ 0x0f

%include "a20.inc"
%include "gdt.inc"

times 510-($-$$) db 0
db 0x55
db 0xAA

我用这个编译它:
nasm -f bin -o boot.bin bootloader.asm

这是kernel.c:
call_main(){main();}
void main(){}

我用这个编译它:
gcc -ffreestanding -o kernel.bin kernel.c

然后:
cat boot.bin kernel.bin > os.bin

我想知道我做错了什么,因为当我用QEMU测试时,它不起作用。有人能给我一些改进kernel.c的建议,这样我就不必使用call_main()函数了吗?
测试时,我使用:
qemu-system-i386 -kernel os.bin

我的其他文件
a20公司:
   enable_A20:
call check_a20
cmp ax, 1
je enabled
call a20_bios
call check_a20
cmp ax, 1
je enabled
call a20_keyboard
call check_a20
cmp ax, 1
je enabled
call a20_fast
call check_a20
cmp ax, 1
je enabled
mov bx, [ERROR]
call print_string
   enabled:
ret


  check_a20:
pushf
push ds
push es
push di
push si

cli

xor ax, ax ; ax = 0
mov es, ax

not ax ; ax = 0xFFFF
mov ds, ax

mov di, 0x0500
mov si, 0x0510

mov al, byte [es:di]
push ax

mov al, byte [ds:si]
push ax

mov byte [es:di], 0x00
mov byte [ds:si], 0xFF

cmp byte [es:di], 0xFF

pop ax
mov byte [ds:si], al

pop ax
mov byte [es:di], al

mov ax, 0
je check_a20__exit

mov ax, 1

 check_a20__exit:
pop si
pop di
pop es
pop ds
popf

ret

    a20_bios:
mov ax, 0x2401
int 0x15
ret

    a20_fast:
in al, 0x92
or al, 2
out 0x92, al
ret

    [bits 32]
    [section .text]

    a20_keyboard:
    cli

    call    a20wait
    mov     al,0xAD
    out     0x64,al

    call    a20wait
    mov     al,0xD0
    out     0x64,al

    call    a20wait2
    in      al,0x60
    push    eax

    call    a20wait
    mov     al,0xD1
    out     0x64,al

    call    a20wait
    pop     eax
    or      al,2
    out     0x60,al

    call    a20wait
    mov     al,0xAE
    out     0x64,al

    call    a20wait
    sti
    ret

    a20wait:
    in      al,0x64
    test    al,2
    jnz     a20wait
    ret


    a20wait2:
    in      al,0x64
    test    al,1
    jz      a20wait2
    ret

gdt公司:
 gdt_start:
dd 0                ; null descriptor--just fill 8 bytes    dd 0 

 gdt_code:
dw 0FFFFh           ; limit low
dw 0                ; base low
db 0                ; base middle
db 10011010b            ; access
db 11001111b            ; granularity
db 0                ; base high

 gdt_data:
dw 0FFFFh           ; limit low (Same as code)
dw 0                ; base low
db 0                ; base middle
db 10010010b            ; access
db 11001111b            ; granularity
db 0                ; base high
  end_of_gdt:

  gdtr: 
dw end_of_gdt - gdt_start - 1   ; limit (Size of GDT)
dd gdt_start            ; base of GDT

   CODE_SEG equ gdt_code - gdt_start
   DATA_SEG equ gdt_data - gdt_start

最佳答案

有很多问题,但总的来说,您的程序集代码是有效的。我写了一个StackOverflow答案,里面有general bootloader development的提示。
不要假设段寄存器设置正确
问题中的原始代码没有设置SS堆栈段寄存器。我给的小费是:
当BIOS跳转到您的代码时,您不能依赖CS、DS、ES、SS、SP
具有有效值或期望值的寄存器。他们应该被建立起来
启动引导加载程序时正确。
如果你需要的话,它也应该被设置。尽管在您的代码中似乎不是这样(除了在print_string函数中,我稍后将讨论)。
正确定义GDT
最大的一个错误是,您在GDT.inc中设置了全局描述符表(GDT),该表的开头是:

gdt_start:
    dd 0                ; null descriptor--just fill 8 bytes    dd 0

每个全局描述符需要8个字节,但dd 0仅定义4个字节(双字)。应该是:
gdt_start:
    dd 0                ; null descriptor--just fill 8 bytes    
    dd 0

实际上,第二个dd 0似乎是意外添加到前一行注释的末尾。
当处于16位实数模式时,不要使用32位代码
您已经编写了一些print_string代码,但它是32位代码:
[bits 32]
    ; prints a null - terminated string pointed to by EBX
print_string :
    pusha
    mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem.
print_string_loop :
    mov al , [ ebx ] ; Store the char at EBX in AL
    mov ah , WHITE_ON_BLACK ; Store the attributes in AH
    cmp al , 0 ; if (al == 0) , at end of string , so
    je print_string_done ; jump to done
    mov [edx] , ax ; Store char and attributes at current
        ; character cell.
    add ebx , 1 ; Increment EBX to the next char in string.
    add edx , 2 ; Move to next character cell in vid mem.
    jmp print_string_loop ; loop around to print the next char.
print_string_done :
    popa
    ret ; Return from the function

在16位代码中调用print_string作为错误处理程序,因此您在这里所做的操作可能会强制重新启动计算机。不能使用32位寄存器和寻址。通过一些调整,可以将代码设置为16位:
    ; prints a null - terminated string pointed to by EBX
print_string :
    pusha
    push es                   ;Save ES on stack and restore when we finish

    push VIDEO_MEMORY_SEG     ;Video mem segment 0xb800
    pop es
    xor di, di                ;Video mem offset (start at 0)
print_string_loop :
    mov al , [ bx ] ; Store the char at BX in AL
    mov ah , WHITE_ON_BLACK ; Store the attributes in AH
    cmp al , 0 ; if (al == 0) , at end of string , so
    je print_string_done ; jump to done
    mov word [es:di], ax ; Store char and attributes at current
        ; character cell.
    add bx , 1 ; Increment BX to the next char in string.
    add di , 2 ; Move to next character cell in vid mem.
    jmp print_string_loop ; loop around to print the next char.

print_string_done :
    pop es                    ;Restore ES that was saved on entry
    popa
    ret ; Return from the function

主要的区别(16位代码)是我们不再使用EAX和EDX 32位寄存器。为了访问视频ram@0xb8000,我们需要使用一个段:偏移对来表示相同的东西。0xb8000可以表示为段:偏移量0xb800:0x0(计算为(0xb800<<4)+0x0)=0xb8000物理地址。我们可以利用这些知识将b800存储在ES寄存器中,并使用DI寄存器作为偏移量来更新视频存储器。我们现在使用:
mov word [es:di], ax

将一个字移到视频内存中。
组装并链接内核和引导加载程序
在构建内核时遇到的一个问题是,不能正确生成可以直接加载到内存中的平面二进制图像。我建议不要使用gcc -ffreestanding -o kernel.bin kernel.c这样做:
gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin

这将使用调试信息将kernel.c汇编为kernel.o(-g)。然后,链接器获取kernel.o(32位ELF二进制文件)并生成一个名为kernel.ELF的ELF可执行文件(如果要调试内核,此文件将非常方便)。然后,我们使用objcopy获取ELF32可执行文件kernel.elf,并将其转换为可以由BIOS加载的平面二进制映像kernel.bin。需要注意的一点是,使用-Tlinker.ld选项时,我们要求LD(linker)从文件linker.LD中读取选项。这是一个简单的linker.ld您可以用来开始:
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)

SECTIONS
{
    . = 0x9000;
    .text : { *(.text) }
    .data : { *(.data) }
    .bss  : { *(.bss) *(COMMON) }
}

这里要注意的是. = 0x9000告诉链接器它应该生成一个可执行文件,该可执行文件将加载到内存地址0x9000。0x9000似乎是你把你的核心放在了你的问题上。其余的代码行提供了需要包含在内核中才能正常工作的C部分。
我建议在使用NASM时做类似的事情,而不是这样做:
nasm -g -f elf32 -F dwarf -o boot.o bootloader.asm
ld -melf_i386 -Ttext=0x7c00 -nostdlib --nmagic -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin

这类似于编译C内核。这里我们不使用链接器脚本,但是我们会告诉链接器生成代码,假设代码(bootloader)将在0x7c00加载。
为此,您需要从bootloader.asm中删除这一行:
[ORG 0x7c00]

清理内核(Kernel.c)
将kernel.c文件修改为:
/* This code will be placed at the beginning of the object by the linker script */    
__asm__ (".pushsection .text.start\r\n" \
         "jmp main\r\n" \
         ".popsection\r\n"
         );

/* Place main as the first function defined in kernel.c so
 * that it will be at the entry point where our bootloader
 * will call. In our case it will be at 0x9000 */

int main(){
    /* Do Stuff Here*/

    return 0; /* return back to bootloader */
}

在bootloader.asm中,我们应该调用nasm -f bin -o boot.bin bootloader.asm函数(将放在0x9000处),而不是跳转到它。而不是:
jmp 0x9000

更改为:
    call 0x9000
    cli
loopend:                ;Infinite loop when finished
    hlt
    jmp loopend

调用后的代码将在C函数main返回时执行。这是一个简单的循环,将有效地停止处理器,并保持这种方式无限期,因为我们没有地方回去。
进行所有建议更改后的代码
引导加载程序.asm:
[bits 16]

global _start
_start:
    cli
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x8000      ; Stack pointer at SS:SP = 0x0000:0x8000
    mov [BOOT_DRIVE], dl; Boot drive passed to us by the BIOS
    mov dh, 17          ; Number of sectors (kernel.bin) to read from disk
                        ; 17*512 allows for a kernel.bin up to 8704 bytes
    mov bx, 0x9000      ; Load Kernel to ES:BX = 0x0000:0x9000

    call load_kernel
    call enable_A20

;   call graphics_mode  ; Uncomment if you want to switch to graphics mode 0x13
    lgdt [gdtr]
    mov eax, cr0
    or al, 1
    mov cr0, eax
    jmp CODE_SEG:init_pm

graphics_mode:
    mov ax, 0013h
    int 10h
    ret

load_kernel:
                        ; load DH sectors to ES:BX from drive DL
    push dx             ; Store DX on stack so later we can recall
                        ; how many sectors were request to be read ,
                        ; even if it is altered in the meantime
    mov ah , 0x02       ; BIOS read sector function
    mov al , dh         ; Read DH sectors
    mov ch , 0x00       ; Select cylinder 0
    mov dh , 0x00       ; Select head 0
    mov cl , 0x02       ; Start reading from second sector ( i.e.
                        ; after the boot sector )
    int 0x13            ; BIOS interrupt
    jc disk_error       ; Jump if error ( i.e. carry flag set )
    pop dx              ; Restore DX from the stack
    cmp dh , al         ; if AL ( sectors read ) != DH ( sectors expected )
    jne disk_error      ; display error message
    ret
disk_error :
    mov bx , ERROR_MSG
    call print_string
    hlt

; prints a null - terminated string pointed to by EDX
print_string :
    pusha
    push es                   ;Save ES on stack and restore when we finish

    push VIDEO_MEMORY_SEG     ;Video mem segment 0xb800
    pop es
    xor di, di                ;Video mem offset (start at 0)
print_string_loop :
    mov al , [ bx ] ; Store the char at BX in AL
    mov ah , WHITE_ON_BLACK ; Store the attributes in AH
    cmp al , 0 ; if (al == 0) , at end of string , so
    je print_string_done ; jump to done
    mov word [es:di], ax ; Store char and attributes at current
        ; character cell.
    add bx , 1 ; Increment BX to the next char in string.
    add di , 2 ; Move to next character cell in vid mem.
    jmp print_string_loop ; loop around to print the next char.

print_string_done :
    pop es                    ;Restore ES that was saved on entry
    popa
    ret ; Return from the function

%include "a20.inc"
%include "gdt.inc"

[bits 32]
init_pm:
    mov ax, DATA_SEG
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov ebp, 0x90000
    mov esp, ebp

    call 0x9000
    cli
loopend:                                ;Infinite loop when finished
    hlt
    jmp loopend

[bits 16]
; Variables
ERROR            db "A20 Error!" , 0
ERROR_MSG        db "Error!" , 0
BOOT_DRIVE:      db 0

VIDEO_MEMORY_SEG equ 0xb800
WHITE_ON_BLACK   equ 0x0f

times 510-($-$$) db 0
db 0x55
db 0xAA

gdt公司:
gdt_start:
    dd 0                ; null descriptor--just fill 8 bytes
    dd 0

gdt_code:
    dw 0FFFFh           ; limit low
    dw 0                ; base low
    db 0                ; base middle
    db 10011010b        ; access
    db 11001111b        ; granularity
    db 0                ; base high

gdt_data:
    dw 0FFFFh           ; limit low (Same as code)
    dw 0                ; base low
    db 0                ; base middle
    db 10010010b        ; access
    db 11001111b        ; granularity
    db 0                ; base high
end_of_gdt:

gdtr:
    dw end_of_gdt - gdt_start - 1   ; limit (Size of GDT)
    dd gdt_start        ; base of GDT

    CODE_SEG equ gdt_code - gdt_start
    DATA_SEG equ gdt_data - gdt_start

a20公司:
enable_A20:
    call check_a20
    cmp ax, 1
    je enabled
    call a20_bios
    call check_a20
    cmp ax, 1
    je enabled
    call a20_keyboard
    call check_a20
    cmp ax, 1
    je enabled
    call a20_fast
    call check_a20
    cmp ax, 1
    je enabled
    mov bx, [ERROR]
    call print_string
enabled:
    ret

check_a20:
    pushf
    push ds
    push es
    push di
    push si

    cli
    xor ax, ax ; ax = 0
    mov es, ax
    not ax ; ax = 0xFFFF
    mov ds, ax
    mov di, 0x0500
    mov si, 0x0510
    mov al, byte [es:di]
    push ax
    mov al, byte [ds:si]
    push ax
    mov byte [es:di], 0x00
    mov byte [ds:si], 0xFF
    cmp byte [es:di], 0xFF
    pop ax
    mov byte [ds:si], al
    pop ax
    mov byte [es:di], al
    mov ax, 0
    je check_a20__exit
    mov ax, 1

check_a20__exit:
    pop si
    pop di
    pop es
    pop ds
    popf
    ret

a20_bios:
    mov ax, 0x2401
    int 0x15
    ret

a20_fast:
    in al, 0x92
    or al, 2
    out 0x92, al
    ret

    [bits 32]
    [section .text]

a20_keyboard:
    cli

    call    a20wait
    mov     al,0xAD
    out     0x64,al
    call    a20wait
    mov     al,0xD0
    out     0x64,al
    call    a20wait2
    in      al,0x60
    push    eax
    call    a20wait
    mov     al,0xD1
    out     0x64,al
    call    a20wait
    pop     eax
    or      al,2
    out     0x60,al
    call    a20wait
    mov     al,0xAE
    out     0x64,al
    call    a20wait
    sti
    ret

a20wait:
    in      al,0x64
    test    al,2
    jnz     a20wait
    ret

a20wait2:
    in      al,0x64
    test    al,1
    jz      a20wait2
    ret

内核c:
/* This code will be placed at the beginning of the object by the linker script */    
__asm__ (".pushsection .text.start\r\n" \
         "jmp main\r\n" \
         ".popsection\r\n"
         );

/* Place main as the first function defined in kernel.c so
 * that it will be at the entry point where our bootloader
 * will call. In our case it will be at 0x9000 */

int main(){
    /* Do Stuff Here*/

    return 0; /* return back to bootloader */
}

链接器.ld
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)

SECTIONS
{
    . = 0x9000;
    .text : { *(.text.start) *(.text) }
    .data : { *(.data) }
    .bss  : { *(.bss) *(COMMON) }
}

使用DD/QEMU调试创建磁盘映像
如果使用上述文件,并使用这些命令生成所需的引导加载程序和内核文件(如前所述)
nasm -g -f elf32 -F dwarf -o boot.o bootloader.asm
ld -melf_i386 -Ttext=0x7c00 -nostdlib --nmagic -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin

gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin

您可以使用以下命令生成磁盘映像(在本例中,我们将使其大小与软盘相同):
dd if=/dev/zero of=disk.img bs=512 count=2880
dd if=boot.bin of=disk.img bs=512 conv=notrunc
dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc

这将创建一个大小为512*2880字节(1.44兆软盘大小)的零填充磁盘映像。main将boot.bin写入文件的第一个扇区,而不截断磁盘映像。dd if=boot.bin of=disk.img bs=512 conv=notrunc将kernel.bin放入从第二个扇区开始的磁盘映像中。dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc在写入之前跳过第一个块(bs=512)。
如果希望运行内核,可以在QEMU中将其作为软盘驱动器A:(seek=1)启动,如下所示:
qemu-system-i386 -fda disk.img

您还可以使用QEMU和GNU调试器(GDB)调试32位内核,其中包含我们在使用上述指令编译/组装代码时生成的调试信息。
qemu-system-i386 -fda disk.img -S -s &
gdb kernel.elf  \
        -ex 'target remote localhost:1234' \
        -ex 'layout src' \
        -ex 'layout reg' \
        -ex 'break main' \
        -ex 'continue'

本例使用远程调试器启动QEMU,并使用文件-fda(我们使用DD创建的)模拟软盘。GDB使用kernel.elf(用debug info生成的文件)启动,然后连接到QEMU,并在C代码中的main()函数处设置断点。当调试器最终就绪时,系统将提示您按disk.img继续。幸运的是,您应该在调试器中查看函数main。

关于assembly - 如何为我的 Bootstrap 制作内核?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55778422/

相关文章:

assembly - 在堆栈上为 execve 创建一个 arg 数组

debugging - mov eax, 大 fs :30h

c - 读取磁盘中的 block (内核编程)

assembly - 设计一个指令序列,使其在使用偏移量解码时执行其他操作

assembly - BSWAP指令 “speed execution of decimal arithmetic”如何?

c - x86 add 和 addl 操作数相加错误?

x86 - 多重引导 1 引导信息总大小

c - linux中的ls命令使用哪个linux系统调用来显示文件夹/文件名?

linux - 未修补的 Linux 内核漏洞

objective-c - 反汇编简单的 ARM 指令?