User Tools

Site Tools


archi:x86

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 :

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 :

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 :

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

Pour aller encore plus loin

archi/x86.txt · Last modified: 2024/03/18 15:06 by 127.0.0.1