Table of Contents
Assembleur x86
Voici quelques notes sur l'assembleur x86 IA32 avec le compilateur GAS (GNU Assembler).
Quelques notes sur x86 IA32 avec GAS
Il existe plusieurs langages de programmation assembleur (GAS, NASM, …) pour générer du code machine x86 32-bit. Nous utilisons ici le langage GAS ou GNU Assembler, qui n'utilise pas la syntaxe Intel contrairement à NASM. Un programme GAS est un fichier texte d'extension '.s' (ou '.S'). La compilation s'effectue avec as et l'édition de lien avec le linker ld (format ELF). On peut aussi utiliser directement le compilateur gcc. L'utilisation du suffixe '.S' indique au compilateur d'effectuer une passe de preprocessing avant la compilation.
- Les registres généralistes sur 32 bits (%eax, %ebx, %ecx, %edx) ont des variantes 16 bits (%ax, %bx, %cx, %dx) et 8 bits (%ah, %al, %bh, %bl, %ch, %cl, %dh, %dl). En pratique, %al est la partie basse de %ax. Respectivement, %ah est la partie haute de %ax.
- On dispose d'opérations classiques : mov, add, sub, and, … qui se déclinent en trois versions selon que l'on manipule des registres 8, 16 ou 32 bits. On ajoute respectivement le suffixe 'b', 'w' ou 'l' à l'instruction. Par exemple, addw %ax,%dx ajoute %ax au registre %dx (en 16 bits).
- Pour faire des comparaisons, on dispose de l'instruction cmp. Par exemple, cmpw %ax,%dx met à jour le flag de condition Z à 1 si %ax est égal à %dx (%dx-%ax=0), de telle sorte que le saut conditionnel je soit vrai. On dispose d'autres sauts conditionnels comme je, jne, jl, jg, …, ainsi que du saut jmp.
- Les constantes sont précécés du symbole $, comme par exemple $1 ou $0x0b8000 ou encore $'A'.
- GAS supporte plusieurs types de commentaires # ... ou // ... ou encore /* ... */.
Plus de détails :
- Compiler Explorer: https://gcc.godbolt.org
Exemple Hello World en GAS
Voici le fameux exemple “Hello World!” :
- hello.s
.text # section declaration (code) .global _start # standard entry point for ELF linker _start: /* write our string to stdout */ movl $len,%edx # third argument: message length movl $msg,%ecx # second argument: pointer to message to write movl $1,%ebx # first argument: file handle (stdout) movl $4,%eax # system call number (sys_write) int $0x80 # call kernel /* and exit */ movl $0,%ebx # first argument: exit code movl $1,%eax # system call number (sys_exit) int $0x80 # call kernel .data # section declaration (data) msg: .ascii "Hello world!\n" # our famous string len = . - msg # string length
Compilons puis exécutons ce petit programme en 32 bits:
# compilation avec as (en 32 bits) as --32 hello.s -o hello.o ld -melf_i386 hello.o -o hello # compilation avec gcc (en 32 bits) gcc -m32 -static hello.s -o hello -nostdlib # execution ./hello Hello world!
Bootloader au format multiboot
Considérons le code assembleur x86 suivant d'un bootloader minimaliste en 32 bits au format multiboot. Le code se compose du bootloader à proprement parler (fichier boot.s), qui appelle la fonction main du kernel (fichier kernel.s).
- boot.s
/* my own bootloader */ .set MAGIC, 0x1BADB002 # magic number (lets bootloader find the header) .set ALIGN, 1<<0 # align loaded modules on page boundaries .set MEMINFO, 1<<1 # provide memory map .set FLAGS, ALIGN | MEMINFO # multiboot flag field .set CHECKSUM, -(MAGIC + FLAGS) # checksum of above, to prove we are multiboot .set STACKSIZE, 0x10000 # stack size /* section .text */ .text .globl _start .type _start,@function .align 4 /* standard multiboot header */ multiboot: .long MAGIC .long FLAGS .long CHECKSUM /* start routine */ _start: movl $(stack + STACKSIZE),%esp # set up stack call main # call main routine hlt # halt .size _start,.-_start /* stack */ .comm stack,STACKSIZE
Pour aller plus loin :
- https://www.gnu.org/software/grub/manual/multiboot/multiboot.html (multiboot format)
- https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format (multiboot header: 0x1BADB002)
Voici maintenant le code de notre petit kernel, qui ne fait rien pour l'instant (nop)
- kernel.s
/* main kernel routine */ .text .globl main main: nop # this kernel does nothing! ret .size main,.-main
Compilons notre petit noyau…
as --32 boot.s -o boot.o as --32 kernel.s -o kernel.o ld -Ttext=0x100000 -e _start -melf_i386 boot.o kernel.o -o kernel
Lors de l'édition de lien, il est important de préciser que le point d'entrée est le symbole '_start' et de placer la séquence magique de 12 octets du bootloader à une adresse supérieure à 0x100000, suivi du code (segment .text). En effet, les adresses < 1MiB sont réservées pour le BIOS, la RAM Video, etc.
On peut aussi utiliser un external linker script pour compiler notre kernel :
ENTRY(_start) SECTIONS { . = 0x100000; .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } }
Puis on effectue l'édition de lien :
ld -T link.ld -melf_i386 boot.o kernel.o -o kernel
Analysons la structure de notre exécutable avec objdump : objdump -D kernel
boot: file format elf32-i386 Disassembly of section .text: 00100000 <multiboot>: 100000: 02 b0 ad 1b 03 00 00 00 fb 4f 52 e4 bc 0010000c <_start>: 10000c: bc 00 00 21 00 mov $0x210000,%esp 100011: e8 01 00 00 00 call 100017 <main> 100016: f4 hlt 00100017 <main>: 100017: 90 nop ... Disassembly of section .bss: 00200000 <stack>: ...
Il ne reste plus qu'à tester notre kernel sur une vraie machine ou à défaut dans QEMU :
qemu-system-i386 -enable-kvm -kernel kernel
L'option -kernel permet de “booter” directement sur un noyau linux au format bzimage ou sur un code au format multiboot comme celui décrit dans cette section. Notons que c'est le format standard supporté par GRUB.
Kernel en C
Si l'on souhaite écrire son kernel en C, pas de problème ! Il faut juste écrire un fichier kernel.c qui définit le symbole main appelé par notre bootlader. En revanche, pas question d'utiliser les bibliothèques standard… Notre code doit être self-content
- kernel.c
void main() { // ... }
Pour le compiler :
as --32 boot.s -o boot.o gcc -m32 -c kernel.c -o kernel.o ld -Ttext=0x100000 -e _start -melf_i386 boot.o kernel.o -o kernel
Un petit kernel qui affiche Hello World !
On remplace le kernel précédent par le code suivant, qui va afficher “Hello World!” dans un écran VGA en mode texte. Les cartes graphiques compatible VGA possédent un mode texte de taille 80×25 caractères. La mémoire vidéo se situe à l'adresse physique 0xB8000-0xB8FFF. Chaque caractère est codé par 2 octets : un premier octet pour le code ASCII du caractère et un second octet pour la couleur.
- kernel.s
/* main kernel routine */ .text # section declaration (code) .globl main main: call cls call clc call printstr ret .size main,.-main /* print string */ printstr: movl $0xb8000,%ebx # set video address (text mode) movl $len,%ecx movl $msg,%esi loop: movb (%esi),%al movb $2,%ah # set green color movw %ax,(%ebx) # print it addl $2,%ebx # next video address addl $1,%esi # next char in string subl $1,%ecx # len-- jne loop ret /* clear screen */ cls: movl $0xb8000,%edi movw $0x0720,%ax movl $80*25,%ecx cld rep stosw ret /* clear cursor */ clc: movw $0x3d4,%dx movb $0xa,%al outb %al,%dx movw $0x3d5,%dx inb %dx,%al andb $0xc0,%al orb $0x1f,%al outb %al,%dx ret .data # section declaration (data) msg: .ascii "Hello world!" # our famous string len = . - msg # string length
On compile enuite notre kernel puis on le lance dans QEMU, comme précédemment, et voici le résultat : un magnifique système d'exploitation “Hello World!” !
qemu-system-i386 -enable-kvm -kernel kernel
Un peu d'aide :
- VGA text-mode (80×25 caractères) : https://en.wikipedia.org/wiki/VGA-compatible_text_mode
- Color in VGA text mode : http://geezer.osdevbrasil.net/osd/cons/index.htm
- Port série (COM) : https://fr.wikipedia.org/wiki/RS-232
Booter son Kernel depuis un CD-ROM
Pour créer un fichier mykernel.iso (image normale d'un CD_ROM à graver) qui boot notre programme kernel ! Voici deux méthodes basées sur GRUB, un multiboot loader, qui va charger notre bootloader+kernel :
Méthode 1 :
mkdir -p iso/boot/grub cp stage2_eltorito iso/boot/grub/stage2_eltorito echo timeout 2 > iso/boot/grub/menu.lst echo title mykernel >> iso/boot/grub/menu.lst echo kernel '(cd)/kernel' >> iso/boot/grub/menu.lst echo boot >> iso/boot/grub/menu.lst cp kernel iso/ genisoimage -R -b boot/grub/stage2_eltorito -no-emul-boot -boot-load-size 4 -boot-info-table -o mykernel.iso iso
Méthode 2 :
mkdir -p iso/boot/grub echo 'menuentry "mykernel" { multiboot /kernel }' > iso/boot/grub/grub.cfg cp kernel iso/ grub-mkrescue -o mykernel.iso iso
Il ne reste plus qu'à le tester dans QEMU :
qemu-system-i386 -cdrom mykernel.iso
Booter son Kernel depuis un disque (sans GRUB)
Pour booter son Kernel depuis un disque (floppy, hard disk ou usb disk), il faut tout d'abord modifier la séquence de boot du BIOS pour qu'il puisse amorcer le système depuis ce périphérique au démarrage de l'ordinateur. Ces 3 types de disque utilisent la même organisation en secteur de 512 octets. Pour rendre un disque bootable, il faut ajouter la signature de boot (i.e. 0x55aa) à la fin du secteur 0.
.code16 # generate 16-bit code .text # executable code location .globl _start; _start: # code entry point #print letter 'H' onto the screen movb $'H' , %al movb $0x0e, %ah int $0x10 #print letter 'e' onto the screen movb $'e' , %al movb $0x0e, %ah int $0x10 #print letter 'l' onto the screen movb $'l' , %al movb $0x0e, %ah int $0x10 #print letter 'l' onto the screen movb $'l' , %al movb $0x0e, %ah int $0x10 #print letter 'o' onto the screen movb $'o' , %al movb $0x0e, %ah int $0x10 hlt # halt . = _start + 510 #mov to 510th byte from 0 pos .byte 0x55 #append boot signature .byte 0xaa #append boot signature
Attention, dans ce cas précis, le BIOS recopie en mémoire physique le secteur 0 (512 octets) et passe la main au CPU pour qu'il commence à exécuter en mode real (16 bits) les instructions au début du secteur 0… Notons que dans ce cas, il est possible de faire appel aux interruptions du BIOS pour écrire en mémoire VGA (int 0x10).
Il faut ensuite compiler ce programme en flat binary (et non en ELF) puis le copier sur sa disquette :
as hello.s -o hello.o ld --oformat=binary hello.o -o hello.bin dd if=/dev/zero of=floppy.img bs=512 count=2880 # standard floppy disk (1.44MB) dd if=hello.bin of=floppy.img
On peut facilement tester ce programme depuis son disque avec QEMU :
qemu-system-i386 -fda floppy.img -boot a # boot on floppy disk qemu-system-i386 -hda floppy.img # boot on hard disk
On peut également mettre son bootloader sur le premier secteur d'une clef USB : /dev/sdc sur ma machine (fdisk -l). Mais, attention de ne pas se tromper au risque de perdre toutes vos données !
cat hello.bin > /dev/sdc
Puis on reboote la “vraie” machine avec la clef USB et voici le résultat. Notons que nous n'avons pas préparé de table des partitions, d'où le message affiché par le BIOS : Invalid Partition Table avant notre fameux Hello World
Pour aller plus loin :
Documentation
- Compilateur as : https://sourceware.org/binutils/docs/as
- Assembleur x86 : https://en.wikibooks.org/wiki/X86_Assembly
- External Linker Script : http://www.emprog.com/support/documentation/thunderbench-Linker-Script-guide.pdf