Quelques frivolités avec gcc (ou "goto vs. appels récursifs")

Auteur :  x0r Publié le   Nombre de commentaires : 2
Mots-clefs : c gcc goto

Rémy m'a envoyé un petit lien que voici, qui explique comment il faudrait écrire un programme qui imprime 100 fois "Hello World" à l'écran, sans utiliser while, for ou do-while.

Cependant, je n'étais pas entièrement convaincu par l'approche récursive, et j'avais le sentiment que c'était moins optimal. Voici une traduction du pseudo-code proposé comme réponse :

#include <stdio.h>

void loop(int n) {
    if (n > 100)
        return;
    else
        printf("Hello World\n");

    loop(n + 1);
}

int main() {
    loop(1);
}

et voici ce que je propose :

#include <stdio.h>

int main() {

    int i = 0;

loop_start:
    printf("Hello World!\n");
    if (++i < 100) goto loop_start;

    return 0;
}   

Je sais ce que vous allez me dire, que goto c'est le diable incarné, tout ça tout ça. Mais comme je suis curieux de savoir comment le compilateur gèrerait les deux codes source, on peut se proposer de comparer les codes assembleur générés.

Pourquoi comparer ? Il est assez aisé d'imaginer qu'on risque d'allouer 100 trames dans la pile si on voulait exécuter la version récursive de cet algorithme, mais qu'on s'en passerait complètement avec la version goto. Mais peut-être que le problème disparaîtrait en expérimentant avec les niveaux d'optimisation de gcc.

J'ai compilé la version goto avec les flags -O0, -O1, -O2 et -O3 par simple curiosité pour ensuite passer un coup d'objdump dessus, et voir comment gcc optimiserait mon code. Voici la version non optimisée (-O0) :

080483c4 <main>:
 80483c4:       55                      push   %ebp
 80483c5:       89 e5                   mov    %esp,%ebp
 80483c7:       83 e4 f0                and    $0xfffffff0,%esp
 80483ca:       83 ec 20                sub    $0x20,%esp
 80483cd:       c7 44 24 1c 00 00 00    movl   $0x0,0x1c(%esp)
 80483d4:       00 
 80483d5:       eb 01                   jmp    80483d8 <main+0x14>
 80483d7:       90                      nop
 80483d8:       c7 04 24 c0 84 04 08    movl   $0x80484c0,(%esp)
 80483df:       e8 14 ff ff ff          call   80482f8 <puts@plt>
 80483e4:       83 44 24 1c 01          addl   $0x1,0x1c(%esp)
 80483e9:       83 7c 24 1c 63          cmpl   $0x63,0x1c(%esp)
 80483ee:       7e e7                   jle    80483d7 <main+0x13>
 80483f0:       b8 00 00 00 00          mov    $0x0,%eax
 80483f5:       c9                      leave  
 80483f6:       c3                      ret    

Là, à l'adresse 0x80483cd, le movl correspond à l'initialisation de ma variable locale i. Pourtant, un registre aurait très bien fait l'affaire. Avec -O1, on voit que gcc attribue le registre %ebx à ma variable i:

080483c4 <main>:
 80483c4:       55                      push   %ebp
 80483c5:       89 e5                   mov    %esp,%ebp
 80483c7:       83 e4 f0                and    $0xfffffff0,%esp
 80483ca:       53                      push   %ebx
 80483cb:       83 ec 1c                sub    $0x1c,%esp
 80483ce:       bb 00 00 00 00          mov    $0x0,%ebx
 80483d3:       c7 04 24 c0 84 04 08    movl   $0x80484c0,(%esp)
 80483da:       e8 19 ff ff ff          call   80482f8 <puts@plt>
 80483df:       83 c3 01                add    $0x1,%ebx
 80483e2:       83 fb 64                cmp    $0x64,%ebx
 80483e5:       75 ec                   jne    80483d3 <main+0xf>
 80483e7:       b8 00 00 00 00          mov    $0x0,%eax
 80483ec:       83 c4 1c                add    $0x1c,%esp
 80483ef:       5b                      pop    %ebx
 80483f0:       89 ec                   mov    %ebp,%esp
 80483f2:       5d                      pop    %ebp
 80483f3:       c3                      ret

Par contre, l'instruction mov $0x0,%ebx aurait pu être remplacée par xor %ebx, %ebx, ce qui fait la même chose en prenant moins de place et en plus rapide. C'est d'ailleurs une technique utilisée dans des shellcodes pour initialiser un registre à zéro en évitant d'introduire des octets nuls dans le code. Avec -O2, c'est effectivement ce qui se passe :

080483d0 <main>:
 80483d0:       55                      push   %ebp
 80483d1:       89 e5                   mov    %esp,%ebp
 80483d3:       83 e4 f0                and    $0xfffffff0,%esp
 80483d6:       53                      push   %ebx
 80483d7:       31 db                   xor    %ebx,%ebx
 80483d9:       83 ec 1c                sub    $0x1c,%esp
 80483dc:       8d 74 26 00             lea    0x0(%esi,%eiz,1),%esi
 80483e0:       83 c3 01                add    $0x1,%ebx
 80483e3:       c7 04 24 c0 84 04 08    movl   $0x80484c0,(%esp)
 80483ea:       e8 09 ff ff ff          call   80482f8 <puts@plt>
 80483ef:       83 fb 64                cmp    $0x64,%ebx
 80483f2:       75 ec                   jne    80483e0 <main+0x10>
 80483f4:       83 c4 1c                add    $0x1c,%esp
 80483f7:       31 c0                   xor    %eax,%eax
 80483f9:       5b                      pop    %ebx
 80483fa:       89 ec                   mov    %ebp,%esp
 80483fc:       5d                      pop    %ebp
 80483fd:       c3                      ret

Remarquez le fameux xor. Avec -O3, cependant, il n'y a aucune différence avec -O2.

Essayons maintenant avec la version récursive de cet algorithme. Voici le code avec -O0 :

080483c4 <loop>:
 80483c4:       55                      push   %ebp
 80483c5:       89 e5                   mov    %esp,%ebp
 80483c7:       83 ec 18                sub    $0x18,%esp
 80483ca:       83 7d 08 64             cmpl   $0x64,0x8(%ebp)
 80483ce:       7f 1c                   jg     80483ec <loop+0x28>
 80483d0:       c7 04 24 d0 84 04 08    movl   $0x80484d0,(%esp)
 80483d7:       e8 1c ff ff ff          call   80482f8 <puts@plt>
 80483dc:       8b 45 08                mov    0x8(%ebp),%eax
 80483df:       83 c0 01                add    $0x1,%eax
 80483e2:       89 04 24                mov    %eax,(%esp)
 80483e5:       e8 da ff ff ff          call   80483c4 <loop>
 80483ea:       eb 01                   jmp    80483ed <loop+0x29>
 80483ec:       90                      nop
 80483ed:       c9                      leave  
 80483ee:       c3                      ret    

080483ef <main>:
 80483ef:       55                      push   %ebp
 80483f0:       89 e5                   mov    %esp,%ebp
 80483f2:       83 e4 f0                and    $0xfffffff0,%esp
 80483f5:       83 ec 10                sub    $0x10,%esp
 80483f8:       c7 04 24 01 00 00 00    movl   $0x1,(%esp)
 80483ff:       e8 c0 ff ff ff          call   80483c4 <loop>
 8048404:       c9                      leave  
 8048405:       c3                      ret

et avec -O2 (en ne montrant que la fonction loop) :

080483d0 <loop>:
 80483d0:       55                      push   %ebp
 80483d1:       89 e5                   mov    %esp,%ebp
 80483d3:       53                      push   %ebx
 80483d4:       83 ec 14                sub    $0x14,%esp
 80483d7:       8b 5d 08                mov    0x8(%ebp),%ebx
 80483da:       83 fb 64                cmp    $0x64,%ebx
 80483dd:       7f 15                   jg     80483f4 <loop+0x24>
 80483df:       90                      nop
 80483e0:       83 c3 01                add    $0x1,%ebx
 80483e3:       c7 04 24 e0 84 04 08    movl   $0x80484e0,(%esp)
 80483ea:       e8 09 ff ff ff          call   80482f8 <puts@plt>
 80483ef:       83 fb 64                cmp    $0x64,%ebx
 80483f2:       7e ec                   jle    80483e0 <loop+0x10>
 80483f4:       83 c4 14                add    $0x14,%esp
 80483f7:       5b                      pop    %ebx
 80483f8:       5d                      pop    %ebp
 80483f9:       c3                      ret    
 80483fa:       8d b6 00 00 00 00       lea    0x0(%esi),%esi

Pour ceux que ça gave de lire de l'assembleur : avec -O0, le code assembleur appelle de manière stupide lui-même 100 fois avec call, ce qui fait aussi 100 créations et destructions de trames de pile. Imaginez si nous avions des millions d'appels récursifs ! Sans parler du fait qu'aucun registre n'est alloué pour les variables locales, alors qu'on n'en a qu'une seule dans notre fonction et qu'elle aurait pu être dans %ebx. Cependant, avec -O2, gcc fait ce qu'on appelle une optimisation des appels terminaux. En effet, nous avons ici affaire à une récursion terminale, ce qui permet d'en faire un code optimisé qui ressemble pas mal à ma version goto avec -O2. Trop fort, gcc !

Bref, en conclusion, la version récursive n'est pas si mauvaise que ça, contrairement à ce que je pensais ; cependant, il faut absolument compiler avec -O2 pour que ce soit réellement intéressant. Mais je souhaite quand même en finir avec la diabolisation systématique du goto qui, lorsqu'il est bien utilisé, peut rendre le code bien plus lisible. Par exemple, pour sortir d'un switch et sortir du while qui le contient sans goto, il faudrait y aller à coups de booléens susceptibles de rendre les conditions de sortie des while en question plus compliqués, et ce juste pour un cas particulier. C'est pourquoi, et plus particulièrement en programmation système, je tiens à garder ce mot-clé dans mon arsenal.

En tout cas, c'est clair qu'on pouvait s'affranchir de goto dans notre exemple (d'ailleurs, c'est pour ça qu'on a des structures de contrôle comme for ou while). Mais regarder comment gcc optimise ce genre de programmes à chaque fois reste une expérience intéressante.

Migration de serveur terminée !

Auteur :  x0r Publié le   Nombre de commentaires : 0
Mots-clefs : site

Si vous voyez ce post, c'est que la migration du site vers mon serveur auto-hébergé est terminée. J'espère pouvoir corriger tous les bugs qui restent, s'il en reste...

Minitel comme terminal Linux

Auteur :  x0r Publié le   Nombre de commentaires : 64
Mots-clefs : linux minitel serie terminal mgetty gentoo vt100

Oui, j'ai été suffisamment taré pour acheter un Minitel 1B Telic lors d'un vide-greniers. Et pour cause : le 30 juin 2012, le réseau Minitel fermera définitivement (snif). Le SIANA organise une exposition portant sur le Minitel ce jour-là, et je serai présent pour y tenir un stand sur ce qu'on peut encore faire de son vieux terminal. Une petite explication ici sur comment un Minitel 1B peut encore servir, même sans avoir de ligne téléphonique fixe chez soi. Je ne ressens pas le besoin d'aller sur 3615 ULLA de toute façon.

Tout d'abord, ce sont l'excellent article de furrtek et la vidéo YouTube correspondante qui m'ont donné envie de me lancer dans cette expérience, d'autant plus que j'ai un serveur qui sert à faire un peu tout (sauf le café), y compris faire son kéké avec son uptime. Brancher un Minitel dessus a donné un résultat très convaincant.

Minitel 1B affichant la page Wikipédia du
Minitel
Oui, les trois dernières lignes sont en néerlandais. Autres questions ?

Cependant, la partie la moins évidente est la fabrication du câble série entre le PC et le Minitel. En effet, non seulement la connectique est différente, mais aussi les niveaux de tension des signaux: le PC utilise des signaux 5 volts, mais le Minitel utilise 12 volts. Il faut donc adapter ces signaux en adaptant les tensions.

Le matériel

Il vous faut grapiller :

  • un bout de veroboard/plaque d'essai/stripboard/ce que vous voulez ;
  • 2 résistances 10 kΩ ;
  • 1 résistance 15 kΩ ;
  • 2 transistors NPN 2N2222A ;
  • un connecteur DB-9 femelle ;
  • du câble en longueur suffisante, avec au moins 5 contacts (on peut évidemment récupérer le câble et le connecteur DB-9 en un coup en achetant un câble série et en coupant un des connecteurs) ;
  • un connecteur DIN-5 45° mâle.

et histoire d'enfoncer des portes ouvertes :

  • de quoi souder ;
  • un PC avec un port série physique (ou un convertisseur USB-série si vous n'en avez pas ou que vous avez peur de bousiller vos ports) et un UNIX digne de ce nom (personnellement j'ai testé sous Gentoo, mais ça devrait fonctionner aussi bien sur n'importe quel Linux ou BSD) ;
  • un Minitel bistandard.

Attention : ceci ne marchera que sur les Minitels dits "bistandards". Vous pouvez les reconnaître à la prise DIN à l'arrière, ainsi qu'à la touche Fnct sur le clavier. La prise DIN seule ne suffit pas !

Pour le schéma, j'ai repris celui de furrtek (en partant du principe que le circuit soit réalisé sur stripboard) :

Circuit et brochage de la prise DIN

Test du câble

Allumez votre Minitel. Il faudra faire un brin de configuration à chaque fois que vous allumez le terminal, le Minitel n'ayant pas de mémoire :

  • Fnct+T A (tapez Fnct et T ensemble, lâchez tout puis tapez sur A) : passage en mode péri-informatique ;
  • Fnct+T E : désactivation de l'echo local
  • Fnct+P 4 : passage en 4800 baud

Sous Linux, installez minicom et lancez en tant que root :

# minicom -s

Dans le menu, configurer le port série pour qu'il soit à 4800 bauds avec 7 bits de données, parité paire et 1 bit d'arrêt. N'oubliez pas de désactiver le flow control hardware, étant donné qu'on n'a pas câblé les broches CTS/RTS côté PC. Tapez des trucs sur le clavier et vérifiez que ça s'affiche bien à l'autre bout sans erreurs de parité.

Installation de getty

L'outil getty est le programme qui va écouter sur le terminal jusqu'à ce que quelqu'un tape un nom de login, et le passe à /bin/login pour l'authentifier. Deux possibilités s'offrent à vous :

La méthode du flemmard

Si vous avez une Debian ou une Gentoo plus ou moins à jour, il y a des chances que le getty que vous avez soit agetty.

Sur Gentoo, ajoutez dans /etc/inittab :

s0:12345:respawn:/sbin/agetty 4800 ttyS0 minitel1b-80

ttyS0 est votre port série (ttyUSB0 si vous utilisez un convertisseur USB-série). Ensuite, faites :

# init q

et si tout marche bien, vous aurez un prompt de login hideux qui ne ressemble à rien et avec plein d'erreurs de parité. En effet, agetty ne sait pas qu'il doit utiliser une parité paire. Il la détecte cependant à partir du nom de login que vous tapez, donc ce n'est pas très grave; par contre, c'est juste très moche. Mais on peut arranger ça.

La méthode du non-flemmard

La vraie méthode pour avoir un prompt de login plus joli est d'utiliser un getty qui sait lire le fichier /etc/gettydefs. En particulier, mgetty sait faire. Problème: l'inteprétation de gettydefs est désactivée par défaut dans le code, ce qui fait que la version fournie par votre distrib' ne servira à rien; il faudra le compiler à partir des sources. Si vous avez une Gentoo, vous pouvez fabriquer un ebuild personnalisé ; sinon, démerdez-vous.

Vérifiez d'abord que vous avez PORTDIR_OVERLAY="/usr/local/portage" dans /etc/make.conf, et ajoutez-le dans le cas contraire. Puis toujours en tant que root :

# mkdir -p /usr/local/portage/net-dialup
# cp -r /usr/portage/net-dialup/mgetty /usr/local/portage/net-dialup
# cd /usr/local/portage/net-dialup/mgetty
# rm Manifest Changelog metadata.xml
# mv mgetty-1.1.36-r3.ebuild mgetty-1.1.36-r4.ebuild
# vi mgetty-1.1.36-r4.ebuild

Dans la fonction src_unpack(), juste après les epatch, ajoutez la ligne:

epatch "${FILESDIR}/${PN}-use-gettydefs.patch"  # use gettydefs

Sauvegardez et quittez. Ensuite, dans files/mgetty-use-gettydefs.patch, mettez-y le patch.

Puis, dans le répertoire de l'ebuild, faites :

# ebuild mgetty-1.1.36-r4.ebuild manifest

et enfin :

# emerge -uva mgetty

Maintenant que vous avez installé et compilé un mgetty proprement, on va ensuite passer aux fichiers de configuration proprement dits. Pour ça, rien de secret. Dans /etc/mgetty+sendfax/mgetty.config, ajoutez :

port ttyS0
 speed 4800
 direct yes
 data-only yes
 toggle-dtr no
 need-dsr yes
 port-owner root
 port-group root
 port-mode 0600
 login-prompt @ \P login:\040
 login-time 60
 term minitel1b-80
 gettydefs 4800v23

et dans le fameux /etc/gettydefs, mettez tout ça sur une seule ligne :

4800v23# B4800 CS7 PARENB -PARODD GLOBAL #B4800 ISTRIP CS7 PARENB 
-PARODD GLOBAL BRKINT IGNPAR ICRNL IXON IXANY OPOST ONLCR CREAD HUPCL 
ISIG ICANON ECHO ECHOE ECHOK #@S login: #4800v23 

puis, dans /etc/inittab, mettez :

s0:12345:respawn:/sbin/mgetty -br ttyS0 4800v23 -i /etc/issue.mgetty

Le fichier /etc/issue contient les messages qui seront affichés juste avant le prompt de login (le nom de machine, l'heure, la version du kernel...). Il se trouve que mgetty et agetty n'utilisent pas les mêmes champs pour faire la même chose. Pour y remédier, il faut dire à mgetty d'utiliser son propre /etc/issue.mgetty bien à lui. Illustration :

# cat /etc/issue

This is \n.\o (\s \m \r) \t

# cat /etc/issue.mgetty 

This is @.mondomaine (\s \m \R) \C

#

Les sauts de ligne sont importants. Je vous laisse vous débrouiller comme un grand pour avoir un issue.mgetty qui marche. Lorsque vous êtes prêts, tapez la commande que vous attendiez avec impatience (ou pas) :

# init q

Il se peut que vous devriez killer le agetty qui s'est attaché sur ttyS0 pour que mgetty puisse prendre la main dessus, si vous avez d'abord testé la méthode du flemmard. En tout cas, si votre Minitel était déjà allumé, vous constaterez que le prompt s'affiche bien. Sinon, allumez-le, tapez les trois combinaisons de touches, et tapez Ctrl+U pour avoir votre prompt. Logguez-vous, et amusez-vous bien !

Le résultat final en vidéo

Conclusion

Il faudra un peu de soudure, mais je pense qu'utiliser un Minitel de cette façon reste un bon moyen de le refaire vivre comme console série. Certes, le clavier n'est peut-être pas des plus confortables ; certes, je n'ai pas (encore) trouvé comment taper des underscore (_) ou des tildes (~) ; mais pour commenter une ligne dans un fichier de config lorsqu'on s'est planté dans un iptables et qu'on a coupé le port 22, ça peut sauver la vie (et donc l'uptime).

Orange a repoussé à plusieurs reprises la clôture de son service Minitel, mais il semblerait bien que le 30 juin 2012 soit la date définitive et qu'elle ne changera plus. Ça n'arrangera pas les irréductibles qui s'en servent encore, mais après (quand même) trente ans, on peut dire que le service a fait son temps.

J'ai un pressentiment, comme quoi le 1er juillet 2012, il y aura beaucoup de vieux Minitels à la rue. Du coup, les voir pourrir le long du trottoir m'écœurera d'autant plus.

Edit : Pour taper les {, |, }, ~, ` et _, utilisez les combinaisons de touches Ctrl+1 à Ctrl+6. Celles-ci ne sont pas indiquées sur le clavier de mon terminal. Pour Backspace, utilisez Ctrl+H ; pour la touche Tab, faites Ctrl+I.

Suite : Minitel sur Raspberry Pi

Sources

Autres liens intéressants

Mon premier ebuild !

Auteur :  x0r Publié le   Nombre de commentaires : 0
Mots-clefs : linux gentoo gohufont sunrise ebuild

Pendant toutes ces années d'utilisation de Gentoo, j'ai enfin voulu rendre quelque chose à la communauté. Ainsi, ma contribution était sous la forme de mon tout premier ebuild, pour une police de caractères que j'aime beaucoup, à savoir Gohufont.

Cet ebuild est maintenant disponible dans l'overlay sunrise, dont je tiens à remercier les développeurs pour leur soutien (et à Gohu pour avoir fait une police que j'utilise maintenant depuis au moins deux ans). Enfin, en guise de trophée, cette screenshot :

Sortie de eix gohufont

Et si ça finit un jour dans l'arbre portage officiel, je vomirai des arcs-en-ciel.

Wacom Bamboo Pen & Touch on Gentoo Linux (part 2)

Auteur :  x0r Publié le   Nombre de commentaires : 0
Mots-clefs : linux gentoo wacom x11

As promised, I'll give you a little follow-up on the custom scripts I've written to make my Wacom tablet work on Gentoo.

The biggest issue I had was pretty much anything with respect to aspect ratio. For instance, I have a ThinkPad with a 1024×768 screen, but the tablet is designed for 16:10 screens.

Checking for correct aspect ratio

The test is simple: grab a coin, fire up GIMP and trace the coin. If you don't get a circle, that means the aspect ratio of the tablet is off, and therefore your drawings will be stretched.

Fixing aspect ratio

Two commands are enough:

xsetwacom set "Wacom Bamboo 16FG 4x5 Pen stylus" "Area" "0 0 12266 9200"
xsetwacom set "Wacom Bamboo 16FG 4x5 Pen eraser" "Area" "0 0 12266 9200"

This will set the active area of the tablet. Its active area is 14720×9200 units, so you can "clip off" a little bit of the tablet's active area in order to preserve your aspect ratio. For more info, this page has some more useful resources.

The big picture

Here's the contents of my ~/myscripts/wacom_init.sh (which I run manually when I hotplug my Wacom tablet):

#!/bin/sh

# Wacom settings: eraser and pen aspect ratio and softer pressure curve
xsetwacom set "Wacom Bamboo 16FG 4x5 Pen stylus" "Area" "0 0 12266 9200"
xsetwacom set "Wacom Bamboo 16FG 4x5 Pen eraser" "Area" "0 0 12266 9200"
xsetwacom set "Wacom Bamboo 16FG 4x5 Pen stylus" "PressureCurve" "0 50 50 100"
xsetwacom set "Wacom Bamboo 16FG 4x5 Pen eraser" "PressureCurve" "0 50 50 100"

# Wacom keys
xsetwacom set "Wacom Bamboo 16FG 4x5 Finger pad" Button 9 "key shift key plus"
xsetwacom set "Wacom Bamboo 16FG 4x5 Finger pad" Button 8 "key shift key minus"

Have fun!