libasm · 42 // TECHNICAL GUIDE
SYS:ONLINE REC // 06
Projet 42 · Branche Système · Assembly

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.

Difficulté
★★★☆☆
Temps estimé
20 – 40 h
Syntaxe
Intel · NASM · x86-64
Sortie
libasm.a
// Contraintes clés

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.

« L'assembleur, c'est parler directement à la machine. Pas d'abstraction, pas de compilateur pour vous protéger. Chaque registre est votre responsabilité, chaque byte compte, et un seul dépassement de pile vous renvoie au néant. »
— Manuel de terrain YoRHa, module 042
01

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

FonctionmanDescription
ft_strlenman 3 strlenLongueur d'une chaîne
ft_strcpyman 3 strcpyCopie d'une chaîne
ft_strcmpman 3 strcmpComparaison de chaînes
ft_writeman 2 writeAppel système write + errno
ft_readman 2 readAppel système read + errno
ft_strdupman 3 strdupmalloc + strcpy
ft_atoi_basepiscineConversion string → int avec base
ft_list_push_frontpiscineInsertion en tête de liste
ft_list_sizepiscineTaille d'une liste
ft_list_sortpiscineTri de liste par fonction cmp
ft_list_remove_ifpiscineSuppression conditionnelle
// errno obligatoire

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.

02

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é.

// Convention d'appel System V AMD64
Passage d'arguments (ordre) : 1er arg → rdi 2e arg → rsi 3e arg → rdx 4e arg → rcx 5e arg → r8 6e arg → r9 7e+ → stack Valeur de retour : rax (pour les entiers/pointeurs) rdx:rax (pour les valeurs 128 bits) Registres callee-saved (à préserver) : rbx, rbp, r12, r13, r14, r15 → Si utilisés, doivent être sauvegardés (push) puis restaurés (pop) avant ret Registres caller-saved (peuvent être clobberés) : rax, rcx, rdx, rsi, rdi, r8, r9, r10, r11 → Détruits par tout call. Sauvegarder sur la stack si nécessaire. Appels système Linux x86-64 : rax = numéro du syscall (0=read, 1=write, ...) rdi, rsi, rdx, r10, r8, r9 = arguments syscall rax = valeur de retour (négatif = -errno si erreur)
// Le piège n°1 du projet

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.

03

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).

srcs/ft_strlen.s// 12 lignes
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.

srcs/ft_strcpy.s// 15 lignes
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.

srcs/ft_strcmp.s// 22 lignes
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.

srcs/ft_write.s// 18 lignes
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.

srcs/ft_read.s// 18 lignes
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.

srcs/ft_strdup.s// 36 lignes
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
04

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.

// Flow de gestion d'erreur syscall
Succès : syscall → rax = nombre de bytes lus/écrits (≥ 0) → return rax directement Échec : syscall → rax = -errno (ex: -9 pour EBADF) → neg rax (convertit -9 → 9) → push rax (sauvegarde la valeur d'errno, car __errno_location écrase rax) → call __errno_location (rax = pointeur vers errno) → pop rcx (récupère la valeur errno) → mov [rax], ecx (*errno = valeur) → mov rax, -1 (return -1) Bug critique : sans le push/pop, rax est détruit par __errno_location ! Le tester Tripouille vérifie errno == EBADF (9) après ft_write(-1, ...)
// Numéros de syscall Linux x86-64

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.

05

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).

includes/libasm.h// structure t_list
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.

srcs/ft_atoi_base.s// validation de base
.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...
// Bug corrigé pendant les tests

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

srcs/ft_list_push_front.s// 26 lignes
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.

srcs/ft_list_size.s// 14 lignes
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.

// Bug critique corrigé dans ft_list_remove_if

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.

06

Bugs & pièges

BugSymptômeSolution
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)
07

Audit fiche d'évaluation

Critère ficheStatutDétail
Bibliothèque = .s files uniquement11 fichiers .s
Makefile ne re-link pasCompilation par .o
main.c lié avec -L. -lasmtests/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_base46/46 OK
Bonus: ft_list_push_front3/3 OK (MOK)
Bonus: ft_list_size4/4 OK
Bonus: ft_list_sort3/3 OK
Bonus: ft_list_remove_if5/5 OK
Pas de segfault
Pas de leak
Tester Tripouille108/108 OK
08

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.

Makefile// extraits clés
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)
// Test rapide

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.