===== 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 ([[http://www.gnu.org/software/binutils/|GAS]], [[http://www.nasm.us/|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 :
* [[https://sourceware.org/binutils/docs/as/ | Doc GAS]]
* [[https://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax|Syntaxe GAS]]
* [[https://en.wikibooks.org/wiki/X86_Assembly|Assembleur x86]]
* Compiler Explorer: https://gcc.godbolt.org
====Exemple Hello World en GAS ====
Voici le fameux exemple "Hello World!" :
.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//).
/* 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)
* https://en.wikibooks.org/wiki/X86_Assembly/Bootloaders
* http://wiki.osdev.org/Bare_bones
* http://wiki.osdev.org/Memory_Map_%28x86%29
* http://files.osdev.org/mirrors/geezer/osd/ram/index.htm
Voici maintenant le code de notre petit kernel, qui ne fait rien pour l'instant (//nop//) ;-)
/* 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 :
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
100016: f4 hlt
00100017 :
100017: 90 nop
...
Disassembly of section .bss:
00200000 :
...
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// ;-)
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 80x25 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.
/* 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
{{ :archi:qemu-sample.png |}}
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// :-)
{{ :archi:helloboot-laptop.jpg?direct&300 |}}
Pour aller plus loin :
* http://www.codeproject.com/Articles/664165/Writing-a-boot-loader-in-Assembly-and-C-Part
* http://www.codeproject.com/Articles/668422/Writing-a-boot-loader-in-Assembly-and-C-Part
* http://www.codeproject.com/Articles/737545/Writing-a-bit-dummy-kernel-in-C-Cplusplus
==== 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
==== Pour aller encore plus loin ====
* http://viralpatel.net/taj/operating-system-tutorial.php
* http://wiki.osdev.org/Main_Page
* http://wiki.osdev.org/Bare_bones
* http://wiki.osdev.org/El-Torito
* https://wiki.archlinux.org/index.php/GRUB
* http://www.syslinux.org/wiki/index.php?title=ISOLINUX
* http://wiki.osdev.org/Mkisofs
* http://wiki.osdev.org/Bootable_CD
* http://stackoverflow.com/questions/34268518/creating-a-bootable-iso-image-with-custom-bootloader