Pour qui désire apprendre la programmation en langage d’assemblage, la plate-forme ARM est une option particulièrement recommandable: cette architecture RISC est relativement simple, la plate-forme est facile d’accès du fait de son vif succès commercial et l’outillage nécessaire à la mise en oeuvre des programmes est mature et bien documenté. Cette série d’articles montre comment on peut utiliser divers éléments de la plate-forme Android pour s’exercer à diverses approches de programmation en langage d’assemblage ARM, facilement et sans aucun coût (sinon un peu de téléchargement), à partir d’une machine Linux.
Dans cette troisième et dernière partie (partie 1, partie 2), j’offre un tutoriel sur le déboguage à distance d’un exécutable binaire avec GDB, le GNU Debugger.
La source de tous les programmes présentés ici peut être accédée en clonant ce
dépôt Git. Le Makefile
inclut avec ce dépôt
compile tous les programmes inclus en invoquant simplement la commande
make
Un serveur de majuscules et de minuscules
Afin de présenter un exemple de programmation en langage d’assemblage un peu plus intéressant que Hello world, j’ai développé une petite application client-serveur. Ce n’est vraiment qu’une preuve de concept: le serveur convertit la casse des chaînes qu’on lui soumet, rien de plus. Le serveur est implanté selon le modèle de programmation sans librairie, alors que le client s’appuie sur libc.
Le protocole de communication entre le client et le serveur est très simple:
il consiste en chaînes de caractères entrecoupées du caractère de fin de ligne
(\n
). Par défaut, une ligne est simplement convertie en lettres majuscules
et retournée au client: ce comportement par défaut peut être testé simplement
à l’aide de netcat
, une
commande disponible sur l’hôte Linux sur lequel on travaille.
En plus de ce traitement par défaut d’une ligne, le serveur accepte trois autres commandes, qui doivent commencer par un caractère spécifique:
- Ascii 0: fermer la connexion et terminer le serveur. Le reste de la ligne n’est pas traité.
- Ascii 1: convertir le reste de la ligne en lettres majuscules. Ceci équivaut au comportement par défaut.
- Ascii 2: convertir le reste de la ligne en lettres minuscules.
Dans les deux derniers cas, seule la partie de la ligne sujette à la conversion est retournée au client.
Le serveur
Le serveur est démarré en spécifiant l’adresse IP et le port sur lesquels il doit attendre une connexion. On peut utiliser l’adresse 0.0.0.0 pour écouter sur toutes les interfaces du système. Ainsi, après avoir compilé le serveur, on le met en place et on en lance l’exécution ainsi:
$ adb push bin/server /data/local/tmp
43 KB/s (3524 bytes in 0.079s)
$ adb shell
root@generic:/ # /data/local/tmp/server 0.0.0.0 9887
Mon serveur attend patiemment une connexion client sur le port 9887. Testons à
l’aide de Netcat, soit la commande nc
. Comme mon émulateur n’a pas sa propre
adresse IP, faisons en sorte de faire suivre les connexions TCP au port
local 9887 vers le même port local de l’émulateur, de sorte qu’une connexion à
l’hôte même corresponde à une connexion sur l’émulateur. Cela est réalisé
grâce à ADB.
$ adb forward tcp:9887 tcp:9887
$ nc localhost 9887
asdf
ASDF
QwErty
QWERTY
<CTRL+D>
Les chaînes en majuscules sont les réponses du serveur; j’ai tapé les autres au clavier. La touche CTRL+D ferme le fichier en entrée de Netcat, ce qui met un terme à la connexion au serveur.
Le client
La fonctionnalité complète du serveur est accessible via le client. Ce dernier
facilite l’accès au protocole en traduisant les mnémoniques #quit
, #upper
et #lower
en les caractères ASCII appropriés du protocole. Il fait aussi en
sorte de sauter les blancs entre la commande et le texte qui y est associé. Ce
client doit aussi rouler sur l’émulateur. On lui passe l’adresse IP et le port
où le serveur écoute:
$ adb push bin/client /data/local/tmp
60 KB/s (6396 bytes in 0.102s)
$ adb shell
root@generic:/ # /data/local/tmp/client 127.0.0.1 9887
Connected!
>
Le >
est l’invite du client, qui montre les réponses du serveur précédées
d’un tiret. Envoyons quelques commandes au serveur:
> asdf
- ASDF
> #upper QwertY
- QWERTY
> #lower QwertY
- qwerty
> #quit
root@generic:/ #
On remarque que cette dernière commande a fait en sorte que le serveur termine son exécution. Pour quitter une session client sans tuer le serveur, on peut simplement taper CTRL+D, comme c’était le cas pour Netcat.
Déboguage à distance avec GDB
Soyons à vouloir déboguer un de nos programmes ARM. Lorsqu’un programme machine se trouve sur la machine où on roule le débogueur, on démarre simplement le débogueur contre le programme. Par contre, si le programme à déboguer est sur une machine distincte, il faut le démarrer sous un serveur de déboguage. En plus de s’attacher à notre programme, ce serveur écoute sur une interface réseau (i.e. adresse IP et port). On démarre ensuite le débogueur sur sa machine de développement et on déclare vouloir déboguer le programme à l’interface spécifiée par le serveur. Une fois cette séquence initiée, le déboguage s’exécute comme si on travaillait localement.
Avant de poursuivre, je mentionne que cet article n’est pas un tutoriel de GDB en tant que tel. Je cherche à démontrer son usage pour déboguer un programme ARM à distance, mais je ne prendrai pas la peine de détailler toutes les commandes de GDB que j’exécute: on se concentre sur le déboguage.
Un bogue dans le traitement de #lower
Pour fins de cette démonstration, retrouvons la branche buggy
du dépôt
Git, recompilons et redéployons le serveur et le
client. Depuis ma copie locale du dépôt:
$ git fetch http://benoithamelin.com/arm_client_server.git buggy:buggy
From http://benoithamelin.com/arm_client_server
* [new branch] buggy -> buggy
$ git checkout buggy
Switched to branch 'buggy'
$ make
/opt/android/ndk/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64/arm-linux-androideabi/bin/as -g -o bin/client.o src/client.s
/opt/android/ndk/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64/arm-linux-androideabi/bin/ld -o bin/client -L/opt/android/ndk/platforms/android-5/arch-arm/usr/lib -dynamic-linker=/system/bin/linker /opt/android/ndk/platforms/android-5/arch-arm/usr/lib/crtbegin_dynamic.o bin/client.o /opt/android/ndk/platforms/android-5/arch-arm/usr/lib/crtend_android.o -lc
$ adb push bin/client /data/local/tmp
139 KB/s (6372 bytes in 0.044s)
$ adb push bin/server /data/local/tmp
72 KB/s (3524 bytes in 0.047s)
Lançons le serveur, puis demandons-lui de convertir une chaîne en lettres minuscules:
$ adb shell
root@generic:/ # cd /data/local/tmp
root@generic:/data/local/tmp # ./server 0.0.0.0 9887
Dans un autre terminal, on lance le client et on lui fait passer notre requête:
$ adb shell
root@generic:/ # cd /data/local/tmp
root@generic:/data/local/tmp # ./client 127.0.0.1 9887
Connected!
> #lower AsdFqwerTY
- AASDFQWERT
>
Ça n’est absolument pas la réponse attendue. Qu’arrive-t-il si on envoie une requête par défaut?
> zxcv
-
xcv
> qwerty
- ZXCV
y
> #quit
root@generic:/data/local/tmp #
Donc, les requêtes de conversion donnent des réponses étrangement farfelues, mais la requête de fermeture du serveur fonctionne (ce qu’on peut constater en jetant un coup d’oeil au terminal du serveur).
On débogue le serveur
Lançons le serveur à nouveau, mais sous l’égide du débogueur, afin d’étudier comment ce programme échange les données. Dans le terminal du serveur, je lance:
root@generic:/data/local/tmp # gdbserver 127.0.0.1:4444 ./server 0.0.0.0 9887
Process ./server created; pid = 1028
Listening on port 4444
J’ouvre un troisième terminal, où je vais exécuter le déboguage.
$ adb forward tcp:4444 tcp:4444
$ /opt/android/ndk/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gdb
GNU gdb (GDB) 7.3.1-gg2
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
<http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=x86_64-linux-gnu
--target=arm-linux-android".
For bug reporting instructions, please see:
<http://source.android.com/source/report-bugs.html>.
(gdb)
Comme on étudie le code du serveur, il faut le charger dans le débogueur. Notez que dans ce troisième terminal où je débogue, je m’étais d’abord placé dans ma copie locale du dépôt Git.
(gdb) file bin/server
Reading symbols from /home/hamelin/Desktop/arm_client_server/bin/server...done.
(gdb) target remote localhost:4444
Remote debugging using localhost:4444
_start () at src/server.s:12
12 bl parse_cmd_line
(gdb)
L’exécution du serveur est donc suspendue sur la première ligne du programme.
L’échange des données avec le client se produit dans la routine
process_conn
, aussi vais-je y poser un point d’arrêt, puis lancer le
programme.
(gdb) break process_conn
Breakpoint 1 at 0x8234: file src/server.s, line 175.
(gdb) c
Continuing.
Je me rends sur le terminal du client, puis je le lance et j’envoie une requête de conversion en minuscules.
root@generic:/data/local/tmp # ./client 127.0.0.1 9887
Connected!
> #lower AsdFqwerTY
Dès que le client s’est connecté, le débogueur a attrapé mon point d’arrêt et m’a donné le contrôle. Je vais tracer le traitement de la connexion de manière à comprendre ce qui se passe lors du traitement de la commande 2, mentionnant les hypothèses derrière mon programme au fur et à mesure.
Breakpoint 1, process_conn () at src/server.s:175
175 mov r4, r0
(gdb) info registers
r0 0x5 5
r1 0x9410 37904
r2 0x9420 37920
r3 0xa 10
r4 0x4 4
r5 0x5 5
r6 0x0 0
r7 0x11d 285
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0x0 0
sp 0xbee0fad0 0xbee0fad0
lr 0x80b4 32948
pc 0x8234 0x8234 <process_conn+4>
cpsr 0x20000010 536870928
(gdb)
À ce moment, le registre R0
contient le description de fichier (FD)
correspondant à la connexion avec le client (socket accepté). Il demeurera
sauvegardé dans R4
.
(gdb) si
176 mov r6, #1
(gdb) si
177 lsl r6, #12
(gdb) si
178 sub sp, r6
(gdb) si
connection_loop () at src/server.s:182
182 mov r0, r4
(gdb)
Je viens d’allouer un tampon de lecture de 4096 octets sur la pile.
182 mov r0, r4
(gdb) si
183 mov r1, sp
(gdb) si
184 mov r2, #1
(gdb) si
185 mov r7, #3
(gdb) si
186 svc 0
(gdb) si
187 cmp r0, #1
(gdb) si
188 blt connection_over_carry_on
(gdb) si
191 ldrb r0, [sp]
(gdb)
J’ai utilisé l’appel système #3 pour lire un unique caractère dans mon tampon (pointé
par SP
). Ce caractère correspond au numéro de la commande à exécutér, ou il
s’agit du premier caractère du texte à convertir selon la commande par défaut.
Si je n’avais pas pu lire ce caractère, j’aurais abandonné la connexion.
191 ldrb r0, [sp]
(gdb) si
192 cmp r0, #32
(gdb) si
193 blt process_command
(gdb) info registers
r0 0x41 65
r1 0xbee0ead0 3202411216
r2 0x1 1
r3 0xa 10
r4 0x5 5
r5 0x5 5
r6 0x1000 4096
r7 0x3 3
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0x0 0
sp 0xbee0ead0 0xbee0ead0
lr 0x80b4 32948
pc 0x8268 0x8268 <connection_loop+36>
cpsr 0x20000010 536870928
(gdb)
Je retrouve dans R0
le caractère lu. Je jette un coup d’oeil à sa valeur
avant de brancher à process_command
: au lieu de la valeur 2 à laquelle je
m’attendais, j’ai la valeur 65. Cela correspond au code ASCII de la lettre A
majuscule… Ainsi, le serveur ne traitera pas ma commande de convertir en
lettres minuscules, mais exécutera plutôt le traitement par défaut (soit la
conversion en majuscules). C’est donc le client qui ne respecte pas le
protocole dont j’avais l’intention. Abandonnons donc le serveur et reprenons
plutôt avec le déboguage du client.
(gdb) kill
Kill the program being debugged? (y or n) y
(gdb)
On voit que l’exécution du serveur (asservie à gdbserver
) est terminée.
Quittons similairement le client (touche Ctrl+D).
On débogue le client
Reprenons d’abord l’exécution du serveur, cette fois sans débogueur:
root@generic:/data/local/tmp # ./server 0.0.0.0 9887
Lançons alors le client sous la tutelle de gdbserver
:
root@generic:/data/local/tmp # gdbserver localhost:4444 ./client 127.0.0.1 9887
Process ./client created; pid = 1039
Listening on port 4444
Depuis le (troisième) terminal où on roule gdb
, il faut maintenant charger
le programme du client à la place de celui du serveur et se préparer à
l’exécution.
(gdb) file bin/client
Load new symbol table from
"/home/hamelin/Desktop/arm_client_server/bin/client"? (y or n) y
Reading symbols from /home/hamelin/Desktop/arm_client_server/bin/client...done.
Error in re-setting breakpoint 1: Function "process_conn" not defined.
(gdb)
Oups! On a encore des points d’arrêt qui n’ont de sens que dans le serveur… Faisons-en table rase.
(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) target remote localhost:4444
Remote debugging using localhost:4444
warning: Unable to find dynamic linker breakpoint function.
GDB will retry eventurally. Meanwhile, it is likely
that GDB is unable to debug shared library initializers
or resolve pending breakpoints after dlopen().
0xb6f0ea40 in ?? ()
(gdb)
Bon, gdb
est un peu mélangé quant à où on se trouve dans le binaire, mais
suggérons-lui un point d’arrêt et lançons l’exécution du client malgré tout.
Le traitement des entrées après l’établissement de la connexion au serveur se
fait sous l’étiquette prompt_loop
.
(gdb) break prompt_loop
Cannot access memory at address 0x0
Breakpoint 2 at 0x85e0: file src/client.s, line 50.
(gdb) c
Continuing.
warning: Could not load shared library symbols for 2 libraries, e.g. /system/bin/linker.
Use the "info sharedlibrary" command to see the complete listing.
Do you need "set solib-search-path" or "set sysroot"?
Breakpoint 2, prompt_loop () at src/client.s:50
50 bl printf
(gdb)
GDB continue ses fanfaronnades, mais il s’est arrêté au bon endroit; un coup d’oeil au terminal du client suggère que nous sommes effectivement connecté et que la ligne 50 où on se trouve va faire afficher l’invite. Traçons donc le code à partir d’ici.
(gdb) ni
51 ldr r0, =buffer
(gdb) ni
52 bl gets
(gdb) ni
Exactement comme prévu. On passe au terminal du client pour entrer notre
commande #lower
.
Connected!
> #lower AsdFqwerTY
Cela nous rend l’invite dans le terminal où roule gdb
.
53 cmp r0, #0
(gdb) si
54 beq done
(gdb) si
56 ldr r5, =buffer
(gdb) si
57 ldrb r1, [r5]
(gdb) si
58 cmp r1, #'#'
(gdb) si
59 bne exchange_data
(gdb) si
60 add r5, #1
(gdb)
Le client reconnaît donc correctement qu’on a entré une commande et qu’il ne faut pas simplement passer notre texte au serveur. Nous allons maintenant analyser de quelle commande il s’agit.
(gdb) si
62 ldr r0, =cmd_quit
(gdb) si
63 mov r1, r5
(gdb) si
64 mov r2, #4
(gdb) si
65 bl strncmp
(gdb) ni
66 beq do_quit
(gdb) info registers
r0 0x5 5
r1 0x98f1 39153
r2 0x4 4
r3 0x0 0
r4 0x4 4
r5 0x98f1 39153
r6 0x3 3
r7 0xb6ef8fd8 3069153240
r8 0x8560 34144
r9 0x0 0
r10 0x0 0
r11 0xbed2baec 3201481452
r12 0x9a3c 39484
sp 0xbed2bab0 0xbed2bab0
lr 0x8618 34328
pc 0x8618 0x8618 <prompt_loop+60>
cpsr 0x20000010 536870928
(gdb)
La valeur retournée par strncmp
est 5, donc pas d’égalité entre la chaîne
tapée (stockée dans un tampon pointé par R5
) et “quit
”. On poursuit.
(gdb) si
68 ldr r0, =cmd_upper
(gdb) si
69 mov r1, r5
(gdb) si
70 mov r2, #5
(gdb) si
71 bl strncmp
(gdb) ni
72 beq do_upper
(gdb) si
74 ldr r0, =cmd_lower
(gdb) si
75 mov r1, r5
(gdb) si
76 mov r2, #5
(gdb) si
77 bl strncmp
(gdb) ni
78 beq do_lower
(gdb) info registers
r0 0x0 0
r1 0x98f1 39153
r2 0x5 5
r3 0x5 5
r4 0x4 4
r5 0x98f1 39153
r6 0x3 3
r7 0xb6ef8fd8 3069153240
r8 0x8560 34144
r9 0x0 0
r10 0x0 0
r11 0xbed2baec 3201481452
r12 0x9a3c 39484
sp 0xbed2bab0 0xbed2bab0
lr 0x8640 34368
pc 0x8640 0x8640 <prompt_loop+100>
cpsr 0x60000010 1610612752
(gdb)
Voilà! On reconnaît donc correctement la commande “#lower
”. Voyons comment
on fait passer cette commande au serveur.
(gdb) si
do_lower () at src/client.s:96
96 mov r1, #2
(gdb)
On garde ici en R1
le numéro de la commande #lower
, soit 2.
(gdb) si
97 mov r2, #5
(gdb)
On place en R2
la longueur de la commande, qu’on veut rejeter de la chaîne
transmise au serveur.
(gdb) si
98 b do_upper_or_lower
(gdb) si
do_upper_or_lower () at src/client.s:101
101 add r5, r2 /* Skip command. */
(gdb) si
102 mov r0, r5
(gdb)
Après avoir rejeté la commande (ligne 101), on se prépare à rejeter les
blancs qui séparent la commande du texte en invocant la routine
skip_whitespace
; son seul paramètre est la chaîne de laquelle on veut
rejeter les blancs.
(gdb) si
102 mov r0, r5
(gdb) si
103 bl skip_whitespace
(gdb) ni
104 mov r5, r0
(gdb) info registers
r0 0x98f7 39159
r1 0x41 65
r2 0x5 5
r3 0x5 5
r4 0x4 4
r5 0x98f6 39158
r6 0x3 3
r7 0xb6ef8fd8 3069153240
r8 0x8560 34144
r9 0x0 0
r10 0x0 0
r11 0xbed2baec 3201481452
r12 0x9a3c 39484
sp 0xbed2bab0 0xbed2bab0
lr 0x8684 34436
pc 0x8684 0x8684 <do_upper_or_lower+12>
cpsr 0x20000010 536870928
(gdb)
Holà! Il est vraisemblable que skip_whitespace
ait fait un bon travail en ce
qui concerne le rejet des blancs, mais cette routine a aussi ruiné la valeur
de R1
, qui devait contenir l’index de la commande à
envoyer au serveur. Poursuivons pour bien voir la chaîne qui sera transmise au
serveur.
(gdb) si
105 mov r0, r4
(gdb) si
106 bl send_command
(gdb) si
send_command () at src/client.s:184
184 push {lr}
(gdb) si
185 sub sp, #1
(gdb) si
186 strb r1, [sp]
(gdb) si
187 mov r1, sp
(gdb) si
188 mov r2, #1
(gdb) si
189 mov r3, #0
(gdb) si
190 bl send
(gdb) info registers
r0 0x4 4
r1 0xbed2baab 3201481387
r2 0x1 1
r3 0x0 0
r4 0x4 4
r5 0x98f7 39159
r6 0x3 3
r7 0xb6ef8fd8 3069153240
r8 0x8560 34144
r9 0x0 0
r10 0x0 0
r11 0xbed2baec 3201481452
r12 0x9a3c 39484
sp 0xbed2baab 0xbed2baab
lr 0x8690 34448
pc 0x8798 0x8798 <send_command+24>
cpsr 0x20000010 536870928
(gdb) x/c 0xbed2baab
0xbed2baab: 65 'A'
(gdb)
Ah, voilà: le caractère envoyé au serveur pour dénoter la commande n’est pas ASCII 2 comme désiré, mais ASCII 65. C’est pourquoi le serveur interprétera la commande comme du texte. Par ailleurs, le client s’attendra à recevoir une réponse de 10 caractères, alors que le serveur aura traité une commande par défaut de 11 caractères, d’où la désynchronisation étrange observée entre les commandes et réponses subséquentes.
On résout simplement ce bogue en sauvegardant la valeur de R1
avant l’appel
de skip_whitespace
, qu’on restaure au retour. La version sans erreur sur la
branche master
du dépôt utilise R7
pour cette sauvegarde. Il faut alors
sauvegarder et restaurer ce registre dans le push
et le pop
qui encadrent
la routine main
dont l’étiquette do_upper_or_lower
fait partie.
Terminons notre séance de déboguage:
(gdb) kill
Kill the program being debugged? (y or n) y
(gdb) quit
$
On force l’interruption du serveur en lui envoyant la touche Ctrl+C.
Conclusion
Ceci complète ce tour d’horizon des outils pour s’exercer à la programmation ARM. Le système Android procure à cette fin un environnement d’exercice assez familier (Linux) pour permettre de programmer des choses intéressantes. En même temps, la distance entre l’hôte et l’émulateur démontre adéquatement le type de difficultés auxquelles font face les développeurs de systèmes embarqués.