assembly - ¿Cómo hacer el núcleo para mi gestor de arranque?
operating-system kernel (1)
Estoy tratando de hacer mi propio sistema operativo personalizado y necesito ayuda con mi código. Este es mi 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
Lo compilo con esto:
nasm -f bin -o boot.bin bootloader.asm
Esto es kernel.c :
call_main(){main();}
void main(){}
Lo compilo con esto:
gcc -ffreestanding -o kernel.bin kernel.c
y entonces:
cat boot.bin kernel.bin > os.bin
Quiero saber qué estoy haciendo mal porque cuando
pruebo
con
QEMU
no funciona.
¿Alguien puede dar algunos consejos para mejorar
kernel.c
para que no tenga que usar la función call_main ()?
Cuando pruebo uso:
qemu-system-i386 -kernel os.bin
Mis otros archivos
a20.inc :
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.inc :
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
Hay una serie de problemas, pero en general su código de ensamblaje funciona. He escrito una respuesta de que tiene consejos para el desarrollo general del cargador de arranque .
No asuma que los registros de segmento están configurados correctamente
El código original en su pregunta no estableció el registro del segmento de pila SS . Consejo # 1 que doy es:
Cuando el BIOS salta a su código, no puede confiar en que los registros CS, DS, ES, SS, SP tengan valores válidos o esperados. Deben configurarse adecuadamente cuando se inicia el gestor de arranque.
Si necesita ES, también debe configurarse. Aunque en su código no parece ser el caso (excepto en la función print_string que discutiremos más adelante).
Defina correctamente el GDT
El error más grande que le habría impedido llegar al modo protegido fue que configuró la tabla de descriptores globales (GDT) en gdt.inc comenzando con:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes dd 0
Cada descriptor global debe tener 8 bytes, pero
dd 0
define solo 4 bytes (palabra doble).
Debería ser:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes
dd 0
En realidad, parece que el segundo
dd 0
se agregó accidentalmente al final del comentario en la línea anterior.
Cuando esté en modo real de 16 bits, no use código de 32 bits
Ha escrito un código
print_string
pero es un código de 32 bits:
[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
Llama a print_string como un controlador de errores en el código de 16 bits, por lo que lo que está haciendo aquí probablemente obligará a reiniciar la computadora. No puede usar los registros y el direccionamiento de 32 bits. El código se puede hacer de 16 bits con algunos ajustes:
; 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
La principal diferencia (en el código de 16 bits) es que ya no usamos registros EAX y EDX de 32 bits. Para acceder al video ram @ 0xb8000 , necesitamos usar un segmento: par compensado que represente lo mismo. 0xb8000 se puede representar como segmento: desplazamiento 0xb800: 0x0 (Calculado como (0xb800 << 4) + 0x0) = 0xb8000 dirección física. Podemos usar este conocimiento para almacenar b800 en el registro ES y usar el registro DI como compensación para actualizar la memoria de video. Ahora usamos:
mov word [es:di], ax
Para mover una palabra al video ram.
Ensamblar y vincular el núcleo y el cargador de arranque
Uno de los problemas que tiene al construir su Kernel es que no genera correctamente una imagen binaria plana que pueda cargarse directamente en la memoria.
En lugar de usar
gcc -ffreestanding -o kernel.bin kernel.c
, recomiendo hacerlo de esta manera:
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
Esto ensambla
kernel.c
a
kernel.o
con información de depuración (
-g
).
El enlazador luego toma
kernel.o
(binario
ELF de
32 bits) y produce un ejecutable
ELF
llamado
kernel.elf
(este archivo será útil si desea depurar su núcleo).
Luego usamos
objcopy
para tomar el archivo ejecutable
ELF32 kernel.elf
y convertirlo en una imagen binaria plana
kernel.bin
que el BIOS puede cargar.
Una cosa clave a tener en cuenta es que con la opción
-Tlinker.ld
le estamos pidiendo al
LD
(enlazador) que lea las opciones del archivo
linker.ld
.
Este es un
linker.ld
simple que puede usar para comenzar:
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
Lo que hay que tener en cuenta aquí es eso
. = 0x9000
. = 0x9000
le dice al enlazador que debe producir un ejecutable que se cargará en la dirección de memoria
0x9000
.
0x9000
es donde parece haber colocado su núcleo en su pregunta.
El resto de las líneas ponen a disposición las secciones
C
que deberán incluirse en su núcleo para que funcionen correctamente.
Recomiendo hacer algo similar cuando use
NASM,
así que en lugar de hacer
nasm -f bin -o boot.bin bootloader.asm
hágalo de esta manera:
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
Esto es similar a compilar el núcleo C. No usamos un script de enlazador aquí, pero le decimos al enlazador que produzca nuestro código suponiendo que el código (cargador de arranque) se cargará a 0x7c00 .
Para que esto funcione, deberá eliminar esta línea de bootloader.asm :
[ORG 0x7c00]
Limpieza del núcleo (kernel.c)
Modifique su archivo kernel.c para que sea:
/* 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 */
}
En
bootloader.asm
deberíamos llamar a la función
main
(que se colocará en 0x9000) en lugar de saltar a ella.
En lugar de:
jmp 0x9000
Cámbielo a:
call 0x9000
cli
loopend: ;Infinite loop when finished
hlt
jmp loopend
El código después de la llamada se ejecutará cuando vuelva la función principal de C. Es un bucle simple que detendrá efectivamente el procesador y permanecerá así indefinidamente, ya que no tenemos a dónde volver.
Código después de hacer todos los cambios recomendados
bootloader.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.inc :
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.inc :
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
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 */
}
linker.ld
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text.start) *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
Crear imagen de disco usando DD / depuración con QEMU
Si utiliza los archivos anteriores y produce el gestor de arranque y los archivos del kernel necesarios con estos comandos (como se mencionó anteriormente)
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
Puede producir una imagen de disco (en este caso la haremos del tamaño de un disquete) con estos comandos:
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
Esto crea una imagen de disco con relleno cero de tamaño 512 * 2880 bytes (el tamaño de un disquete de 1,44 megabytes).
dd if=boot.bin of=disk.img bs=512 conv=notrunc
escribe
boot.bin
en el primer sector del archivo sin truncar la imagen del disco.
dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc
coloca
kernel.bin
en la imagen del disco comenzando en el segundo sector.
La
seek=1
salta el primer bloque (bs = 512) antes de escribir.
Si desea ejecutar su núcleo, puede iniciarlo como unidad de disquete A: (
-fda
) en
QEMU de
esta manera:
qemu-system-i386 -fda disk.img
También puede depurar su núcleo de 32 bits utilizando QEMU y el Depurador GNU ( GDB ) con la información de depuración que generamos al compilar / ensamblar el código con las instrucciones anteriores.
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''
Este ejemplo inicia
QEMU
con el depurador remoto y emula un disquete usando el archivo
disk.img
(que creamos con
DD
).
GDB se
inicia usando kernel.elf (un archivo que generamos con información de depuración), luego se conecta a
QEMU
y establece un punto de interrupción en la
función
main () en el código
C.
Cuando el depurador finalmente esté listo, se le pedirá que presione
<return>
para continuar.
Con suerte, debería estar viendo la función
main
en el depurador.