Charger un secteur
Objectif : Charger un secteur du disque dur en mémoire et lui passer la main pour poursuivre l’exécution.
Sortir du boot sector
Le BIOS charge le secteur 0 et c’est tout. C’est à nous d’assurer la suite. Une solution : faire deux fichiers et les concaténer pour former une image disque. Le premier fichier est notre bon vieux boot sector, le second est un programme (rien de plus que l’écriture d’un message pour l’instant). Le boot sector charge en mémoire le secteur contenant le programme, puis y saute. On appelle communément ce code chargé par le boot sector le stage 2. Aujourd’hui notre stage 2 tient dans un seul secteur, mais plus tard rien ne l’empêchera d’être aussi volumineux que nécessaire.
Le programme message
Le programme message est presque un copier/coller du boot sector de l’article précédent.
org 0x9000 ; (1)
bits 16
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
mov si, message
call print_string
jmp $
message db 'BLONK! 0.0.3', 0
print_string:
lodsb
or al, al
jz .done
mov ah, 0x0e
mov bx, 0x0007
int 0x10
jmp print_string
.done:
ret
times 512-($-$$) db 0 ; (2)(1) org 0x9000
C’est la grosse différence avec notre précédent boot sector. Le programme sera chargé en 0x9000 alors que le boot sector est en 0x7c00. Notez que le boot sector chargé en 0x7c00 n’est pas un choix, c’est une contrainte. C’est comme ça que fonctionne un PC. Si j’ai choisi 0x9000 pour charger le programme, c’est avant tout parce que c’est possible. J’aurais pu choisir beaucoup d’autres adresses.
(2) times 512-($-$$) db 0
Je m’assure que le fichier binaire fasse exactement 512 octets.
La mémoire du PC au démarrage
Au démarrage du PC, en mode réel, le CPU n’a pas accès à plus de 1 Mo de mémoire. Entre 0x500 inclus et 0x80000 exclus (un peu plus de 500 Ko) la mémoire est libre, excepté les 512 octets occupés par le boot sector. En dessous c’est pour le BIOS et le CPU. Au dessus c’est pour le BIOS et la mémoire vidéo.
Le nouveau boot sector
BOOT_ADDRESS equ 0x7c00 ; (1)
SHELL_ADDRESS equ 0x9000
DRIVE_NUMBER equ 0x80
org BOOT_ADDRESS
bits 16
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BOOT_ADDRESS
cld
drive_reset: ; (2)
mov ah, 0
mov dl, DRIVE_NUMBER
int 0x13
jc drive_reset
mov ax, SHELL_ADDRESS / 16 ; (3)
mov es, ax
xor bx, bx
mov ah, 0x02 ; (4)
mov al, 1
mov ch, 0
mov cl, 2
mov dh, 0
mov dl, DRIVE_NUMBER
int 0x13 ; (5)
jc drive_reset
jmp 0:SHELL_ADDRESS ; (6)
; ----------------------------------------------------------------------
; Table des partitions
times 446-($-$$) nop
db 0x80 ; Partition active
db 0 ; Starting head
db 2 ; Starting sector
db 0 ; Starting cylinder
db 0x20 ; System ID
db 1 ; Ending head
db 0x10 ; Ending sector
db 0x10 ; Ending cylinder
dd 1 ; LBA ?
dd 131072 ; Total de secteurs (64 MB)
times 510-($-$$) db 0
dw 0xaa55(1) BOOT_ADDRESS equ 0x7c00
C’est la définition d’une constante avec nasm.
(2) drive_reset:
mov ah, 0 ; Fonction "RESET DISK SYSTEM" de l'interruption 13h
mov dl, DRIVE_NUMBER ; Le numéro du drive (chaque disque dur à son numéro)
int 0x13
jc drive_reset ; Si erreur, on retente
Avant de lire on demande au disque dur de faire une remise à zéro de sa (ou ses) tête de lecture. Pas sûr que ce soit encore nécessaire avec des BIOS moderne, ou avec l’émulation disque dur d’une clé USB. Mais historiquement les contrôleurs de disque, ou de disquette, étaient vraiment cheap et ne gardaient pas forcement la mémoire de la position de la tête de lecture. Il fallait donc toujours la replacer sur le secteur 0 avant de la déplacer sur le secteur voulu.
(3) mov ax, SHELL_ADDRESS / 16
mov es, ax
xor bx, bx
On fait en sorte que le couple ES:BX pointe sur l’adresse où on veut loger notre programme. Pourquoi ? Parce que c’est ce que demande la fonction du BIOS qu’on va utiliser.
(4) mov ah, 0x02
mov al, 1 ; Nombre de secteurs à lire
mov ch, 0 ; Cylindre (_cylinder_)
mov cl, 2 ; Numéro du secteur
mov dh, 0 ; Tête (_head_)
mov dl, DRIVE_NUMBER
On continue de charger les registres pour pouvoir utiliser la fonction de chargement de secteur(s) du BIOS. Voir plus loin les explications pour cylinder/head/sector.
(5) int 0x13
jc drive_reset
Quand les registres sont prêts on appelle la fonction de lecture. Et si elle échoue on reprend à partir du reset, c’est plus sûr.
(6) jmp 0:SHELL_ADDRESS
Le programme a été chargé, on peut y «sauter». On se rappelle qu’en mode réel toute
adresse est en réalité un couple segment:offset. Cela signifie que jmp 0x9000 n’ira
pas à l’adresse 0x9000 mais à l’adresse pointée par le couple CS:0x9000. CS étant
le Code Segment. On n’a jamais chargé ce registre et le BIOS ne garanti pas sa
valeur. C’est pourquoi on fait jmp 0:0x9000. Là on est sûr d’aller à l’adresse
0x9000. De plus, par effet de bord, le processeur va forcer le chargement de CS à 0.
CHS : Cylinder / Head / Sector
En français : cylinder, tête, secteur.
Les premiers PCs étaient chers. Un moyen de d’abaisser le coût était “d’oublier” le contrôleur de disquette. Un disque (dur ou disquette) est constitué physiquement de cylindres partagés en plusieurs secteurs sur plusieurs faces accéssibles par plusieurs têtes de lecture. Résultat : au lieu de lire simplement le secteur n° X, on doit lire le secteur S du cylindre C qui se trouve sur la face/tête H.
Assemblage et tests
Cette fois on a deux programmes à assembler :
nasm message.nasm -f bin -o message
nasm boot.nasm -f bin -o boot
On les réunit ensemble. L’ordre est important, le boot sector doit absolument être en premier :
cat boot message > kernel
Le nom kernel est un peu prétentieux à ce stade, mais c’est bien vers quoi on
se dirige ;) On peut s’assurer que le kernel fait bien exactement 1024 octets,
la taille de deux secteurs :
ls -l
-rw-rw-r-- 1 xavier xavier 512 Aug 12 15:00 boot
-rw-rw-r-- 1 xavier xavier 1024 Aug 12 15:00 kernel
-rw-rw-r-- 1 xavier xavier 512 Aug 12 15:00 message
On lance dans Qemu pour un retour rapide :
qemu-system-i386 -hda kernel
Pour tester sur un véritable ordinateur (ou comme moi sur plusieurs) on copie d’abord le binaire sur une clé USB :
sudo fdisk -l # <<< pour connaître le device
sudo cp kernel /dev/sdx # <<< remplacez x par la bonne lettre ;)
Attention avec sudo. Si vous ne comprenez pas ce que vous faites, il n’y a pas de mal à se contenter pour l’instant de l’émulateur.
Conclusion
On a vu comment charger un programme de la taille d’un secteur en mémoire à partir d’une clé USB et à lancer ce programme. Au passage on en a appris un peu plus sur l’organisation de la mémoire du PC et sur la géométrie des disques (CHS).
Glossaire
CHS — Cylinder Head Sector
Méthode historique pour parler à un disque dur ou à un lecteur de disquette en mode réel.
Références
- Code complet de BLONK! v0.0.3
- x86 Memory Map
- int 13h/ah=00h
- int 13h/ah=02h
- CHS
- Rolling Your Own Bootloader
Cet article fait partie d’une série :
Commentaires
Pas encore trouvé de solution simple et non-invasive pour avoir des commentaires sur le blog. En attendant vous pouvez laisser votre commentaire et/ou engager une discussion sur mastodon@lkdjiin ou twitter@lkdjiin