libasm
// Assembly yourself — écrire du C en assembleur x86-64, un registre à la fois.
Le projet libasm demande de réécrire six fonctions de la libc
en assembleur x86-64 avec la syntaxe Intel et NASM.
Au programme : manipulations de chaînes (strlen,
strcpy, strcmp,
strdup), appels système (write,
read) avec gestion d'errno, et
bonus avec listes chaînées et conversion de base. Ce guide décortique chaque fonction
avec le code ASM réel, la convention d'appel, et les bugs subtils.
Code 64 bits uniquement. Syntaxe Intel (pas AT&T). Compilation avec
nasm -f elf64. Pas d'inline assembly — fichiers
.s séparés. Respect de la convention d'appel
System V AMD64 ABI : les registres callee-saved
(rbx, rbp, r12-r15) doivent être préservés. Les
appels système doivent correctement setter errno via
__errno_location. La libft n'est pas autorisée —
malloc peut être appelé via extern.
— Manuel de terrain YoRHa, module 042
Le projet
libasm est un projet d'introduction à l'assembleur.
L'objectif : réécrire 6 fonctions de la libc en assembleur x86-64, plus 5 fonctions
bonus : 4 sur les listes chaînées et ft_atoi_base (conversion de base). Le projet se compile en une bibliothèque statique
libasm.a qui peut être liée à un programme C de test.
Fonctions à implémenter
| Fonction | man | Description |
|---|---|---|
ft_strlen | man 3 strlen | Longueur d'une chaîne |
ft_strcpy | man 3 strcpy | Copie d'une chaîne |
ft_strcmp | man 3 strcmp | Comparaison de chaînes |
ft_write | man 2 write | Appel système write + errno |
ft_read | man 2 read | Appel système read + errno |
ft_strdup | man 3 strdup | malloc + strcpy |
ft_atoi_base | piscine | Conversion string → int avec base |
ft_list_push_front | piscine | Insertion en tête de liste |
ft_list_size | piscine | Taille d'une liste |
ft_list_sort | piscine | Tri de liste par fonction cmp |
ft_list_remove_if | piscine | Suppression conditionnelle |
Les fonctions ft_write et ft_read
doivent setter errno en cas d'erreur d'appel système.
Le tester Tripouille vérifie explicitement la valeur d'errno
après chaque appel — c'est le critère le plus testé du projet.
Convention d'appel x86-64
Avant d'écrire une seule ligne d'assembleur, il faut comprendre la convention d'appel System V AMD64 ABI utilisée par Linux. Cette convention définit comment les arguments sont passés, quelle est la valeur de retour, et quels registres doivent être préservés par l'appelé.
Après un call vers une fonction C (malloc, free,
__errno_location), les registres caller-saved
(rax, rcx, rdx, rsi, rdi, r8-r11) sont détruits.
Si vous stockez un pointeur important dans rax et
appelez free ou malloc,
votre pointeur est perdu. Sauvegardez toujours sur la stack avant un
call.
Fonctions mandatory
ft_strlen
La fonction la plus simple : compter les caractères d'une chaîne jusqu'au
terminateur nul. On utilise rdi (1er argument =
pointeur de chaîne) et on incrémente rax (compteur +
valeur de retour).
section .text
global ft_strlen
ft_strlen:
xor rax, rax ; rax = 0 (compteur)
.loop:
cmp byte [rdi + rax], 0 ; si char == '\0'
je .done
inc rax
jmp .loop
.done:
ret ; retourne rax = longueur
ft_strcpy
Copie byte par byte de rsi (source) vers
rdi (destination). La valeur de retour doit être le
pointeur destination original, donc on sauvegarde rdi
dans rax au début.
ft_strcpy:
mov rax, rdi ; save dst (return value)
.loop:
mov cl, byte [rsi] ; load src byte
mov byte [rdi], cl ; store to dst
cmp cl, 0 ; null terminator?
je .done
inc rdi
inc rsi
jmp .loop
.done:
ret
ft_strcmp
Compare deux chaînes caractère par caractère. Retourne la différence
(s1[i] - s2[i]) au premier caractère différent, ou 0
si identiques. On utilise movzx pour zero-extend
les bytes en 64 bits avant la soustraction.
ft_strcmp:
.loop:
mov cl, byte [rdi] ; s1[i]
mov dl, byte [rsi] ; s2[i]
cmp cl, dl
jne .diff
cmp cl, 0 ; both null?
je .equal
inc rdi
inc rsi
jmp .loop
.diff:
movzx rax, cl ; zero-extend s1[i]
movzx rcx, dl ; zero-extend s2[i]
sub rax, rcx ; return s1[i] - s2[i]
ret
.equal:
xor rax, rax ; return 0
ret
ft_write
Wrapper autour du syscall write (numéro 1). Les
arguments sont déjà dans les bons registres (rdi=fd,
rsi=buf, rdx=count), il suffit de mettre 1 dans
rax et d'appeler syscall.
En cas d'erreur, le kernel retourne -errno — il faut
le convertir en errno positif via neg rax et le
stocker via __errno_location.
section .text
global ft_write
extern __errno_location
ft_write:
mov rax, 1 ; syscall write = 1
syscall
cmp rax, 0
jge .success ; succès si >= 0
neg rax ; -errno → +errno
push rax ; save errno value
call __errno_location ; rax = &errno
pop rcx ; restore errno value
mov dword [rax], ecx ; *errno = errno_value
mov rax, -1 ; return -1
.success:
ret
ft_read
Identique à ft_write mais avec le syscall 0 (read).
Même gestion d'errno.
ft_read:
mov rax, 0 ; syscall read = 0
syscall
cmp rax, 0
jge .success
neg rax ; -errno → +errno
push rax
call __errno_location
pop rcx
mov dword [rax], ecx
mov rax, -1
.success:
ret
ft_strdup
La plus complexe des mandatory : calcule la longueur, appelle
malloc(len+1), copie la chaîne. On doit sauvegarder
le pointeur source sur la stack car malloc détruit
rsi.
ft_strdup:
push rdi ; save src (rsi will be clobbered by malloc)
mov rsi, rdi
xor rcx, rcx
.strlen_loop:
cmp byte [rsi + rcx], 0
je .strlen_done
inc rcx
jmp .strlen_loop
.strlen_done:
lea rdi, [rcx + 1] ; malloc(len + 1)
call malloc
pop rsi ; restore src into rsi
test rax, rax ; malloc failed?
je .fail
mov rdi, rax ; rdi = dst
.copy_loop:
mov cl, byte [rsi]
mov byte [rdi], cl
cmp cl, 0
je .copy_done
inc rsi
inc rdi
jmp .copy_loop
.copy_done:
ret ; return dst (rax)
.fail:
ret ; return NULL
Syscalls & errno
Le sous-système le plus critique du projet. En C, write
et read sont des wrappers glibc qui gèrent
automatiquement errno. En assembleur, vous appelez
directement le kernel via syscall — c'est à vous de
convertir le code d'erreur.
read = 0, write = 1,
open = 2, close = 3,
fork = 57, execve = 59,
mmap = 9. La liste complète est dans
/usr/include/asm/unistd_64.h.
Fonctions bonus
Les bonus implémentent des fonctions de liste chaînée et de conversion de base.
La structure t_list fait 16 bytes (2 pointeurs 64 bits).
typedef struct s_list
{
void *data; /* offset 0 — 8 bytes */
struct s_list *next; /* offset 8 — 8 bytes */
} t_list; /* total: 16 bytes */
ft_atoi_base
Convertit une chaîne en entier avec une base arbitraire. La validation de la base
est le point critique : +, -,
les espaces (ASCII 9-13, 32) et les doublons rendent la base invalide. Les
caractères de contrôle (ASCII 8, 14) sont valides.
.check_dup_i:
cmp rcx, rbx ; test de borne de boucle
jge .check_dup_done
mov al, byte [r13 + rcx]
cmp al, '+' ; + interdit
je .ret_zero
cmp al, '-' ; - interdit
je .ret_zero
cmp al, ' ' ; space interdit
je .ret_zero
cmp al, 9 ; si < 9 → valide (control chars)
jl .check_next
cmp al, 13 ; si 9-13 → interdit (whitespace)
jle .ret_zero
.check_next: ; vérifier doublons...
Le tester Tripouille teste une base {'0', '1', 14, 0}
(caractère ASCII 14 = SO). Notre code rejetait initialement tout caractère
ASCII < 9. Fix : seuls +, -,
espace (32) et whitespace (9-13) sont invalides. Les autres caractères (y compris
les control chars) sont valides.
Listes chaînées
ft_list_push_front
ft_list_push_front:
push rdi ; save begin_list
push rsi ; save data
mov rdi, 16 ; malloc(16) = sizeof(t_list)
call malloc
pop rsi ; rsi = data
pop rdi ; rdi = begin_list
test rax, rax ; malloc failed?
je .fail
mov [rax], rsi ; new->data = data
mov rcx, [rdi] ; old head
mov [rax + 8], rcx ; new->next = old head
mov [rdi], rax ; *begin_list = new
.fail:
ret
ft_list_size
La plus simple des bonus : parcourir la liste et compter les nœuds.
ft_list_size:
xor rax, rax ; counter = 0
.loop:
test rdi, rdi ; if (list == NULL)
je .done
inc rax
mov rdi, [rdi + 8] ; list = list->next
jmp .loop
.done:
ret
ft_list_sort
Tri à bulles par swap des pointeurs data (pas des
nœuds entiers). La fonction cmp est appelée via
call r13 — il faut sauvegarder les registres
callee-saved utilisés.
ft_list_remove_if
Supprime les nœuds dont data matche
data_ref selon cmp.
Appelle free_fct(data) puis free(node).
Le bug critique : en mid_loop, le pointeur du
nœud à libérer (dans rax, caller-saved) est détruit
par call r15. En head_loop,
le pointeur est dans rbx (callee-saved, donc préservé),
mais le code sauvegarde quand même [rbp-48] pour
cohérence.
Après call r15 (free_fct), rax
est détruit (cas de mid_loop où le nœud est dans
rax). Si rax contenait le
pointeur du nœud à libérer, le call free suivant
libère une adresse invalide → segfault. En head_loop,
le nœud est en rbx (callee-saved, donc préservé), mais
la même sauvegarde [rbp-48] est utilisée par
cohérence. Solution : sauvegarder le nœud sur la pile
[rbp-48] avant chaque call.
Bugs & pièges
| Bug | Symptôme | Solution |
|---|---|---|
| errno négatif | Test errnocheck KO (errno = -9 au lieu de 9) | neg rax avant de stocker errno (le kernel retourne -errno) |
| Pointeur de nœud clobberé par call | Segfault dans ft_list_remove_if | Sauvegarder le nœud (rax en mid_loop, rbx en head_loop) sur la pile [rbp-48] avant chaque call |
| Validation base trop stricte | ft_atoi_base KO avec base contenant ASCII 8 ou 14 | Seuls +, -, espace (32) et whitespace (9-13) sont invalides |
| rsi clobberé par malloc | ft_strdup copie depuis une adresse invalide | push rdi avant malloc, pop rsi après |
| PIE linking | relocation R_X86_64_PC32 ... can not be used when making a PIE object |
Linker avec -no-pie |
| Callee-saved non préservés | Comportement indéfini, résultats aléatoires | Push rbp, rbx, r12-r15 au début, pop à la fin (ordre inverse) |
| dword vs qword pour errno | errno mal défini + corruption mémoire adjacente (écriture 8 bytes sur un int 4 bytes) | mov dword [rax], ecx (errno est un int 32 bits) |
Audit fiche d'évaluation
| Critère fiche | Statut | Détail |
|---|---|---|
| Bibliothèque = .s files uniquement | ✅ | 11 fichiers .s |
| Makefile ne re-link pas | ✅ | Compilation par .o |
| main.c lié avec -L. -lasm | ✅ | tests/main.c |
| errno correctement setté | ✅ | ERRNOOK sur tous les tests |
| ft_strlen (empty + long) | ✅ | 2/2 OK |
| ft_strcpy (empty + long) | ✅ | 4/4 OK |
| ft_strcmp (empty, ordre) | ✅ | 7/7 OK |
| ft_write (stdout, fd, wrong fd) | ✅ | 15/15 OK (ERRNOOK) |
| ft_read (stdin, fd, wrong fd) | ✅ | 13/13 OK (ERRNOOK) |
| ft_strdup (empty + long) | ✅ | 6/6 OK (MOK = malloc size OK) |
| Bonus: ft_atoi_base | ✅ | 46/46 OK |
| Bonus: ft_list_push_front | ✅ | 3/3 OK (MOK) |
| Bonus: ft_list_size | ✅ | 4/4 OK |
| Bonus: ft_list_sort | ✅ | 3/3 OK |
| Bonus: ft_list_remove_if | ✅ | 5/5 OK |
| Pas de segfault | ✅ | |
| Pas de leak | ✅ | |
| Tester Tripouille | ✅ | 108/108 OK |
Makefile
Le Makefile compile les fichiers .s avec
nasm -f elf64 et crée la bibliothèque statique
libasm.a avec ar rcs.
La règle bonus ajoute les fichiers bonus.
NAME = libasm.a
NASM = nasm
NASMFLAGS = -f elf64
SRCS = $(SRCS_DIR)/ft_strlen.s \\
$(SRCS_DIR)/ft_strcpy.s \\
$(SRCS_DIR)/ft_strcmp.s \\
$(SRCS_DIR)/ft_write.s \\
$(SRCS_DIR)/ft_read.s \\
$(SRCS_DIR)/ft_strdup.s
BONUS_SRCS = $(SRCS_DIR)/ft_atoi_base.s \\
$(SRCS_DIR)/ft_list_push_front.s \\
$(SRCS_DIR)/ft_list_size.s \\
$(SRCS_DIR)/ft_list_sort.s \\
$(SRCS_DIR)/ft_list_remove_if.s
# Compile .s → .o avec nasm
%.o: %.s $(HEADERS)
$(NASM) $(NASMFLAGS) $< -o $@
# Archive → libasm.a
$(NAME): $(OBJS) $(HEADERS)
ar rcs $(NAME) $(OBJS)
# Bonus: ajoute les .o bonus à l'archive
bonus: $(OBJS) $(BONUS_OBJS) $(HEADERS)
ar rcs $(NAME) $(OBJS) $(BONUS_OBJS)
make test compile tests/main.c
avec libasm.a et lance tous les tests. Sur les machines
42, nasm est déjà installé. Sur notre environnement,
nous l'avons extrait d'un paquet .deb.