Que faire quand Go dit "no Go" ?

Il y a quelques jours, à l'occasion de l'AWS Summit 2014, DamZ a aiguisé à neuf mon intérêt longtemps émoussé pour le langage Google Go, avec des nouvelles mirifiques à propos de son utilisation dans l'infrastructure du nouveau service Platform que Commerce Guys lançait le même jour, de sorte que j'ai fait mes devoirs en me remettant à jour sur la programmation Go: après tout, c'est bien à ça que servent les vacances, non ?

Les symptômes

Ce matin arrivait donc le moment de déployer en production, mais hélas, les programmes qui fonctionnaient normalement sur mon portable de développement Ubuntu et sur le serveur de test se refusaient à s'exécuter sur le serveur cible, sous Debian Squeeze, avec des résultats bien mystérieux:

  • Portable: uname -a : Linux ubuntu 3.2.0-64-generic #97-Ubuntu SMP Wed Jun 4 22:04:21 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
  • Cible: uname -a : Linux v1085 3.2.23-vs2.3.2.12-beng #1 SMP Tue Jul 24 10:15:16 BST 2012 x86_64 GNU/Linux
  • binaire compilé copié d'une machine à l'autre, avec ses fichiers de configuration, son schéma de base de données, et les droits configurés sur les deux machines
  • Tout était OK sur le portable.
$ ls -l myapp
-rwxrwxr-x 1 root root 3522952 Jun  6 11:32 myapp
$ ./myapp
bash: ./myapp: no such file or directory
$ strace -f -e open ./myapp
exec: no such file or directory
$ file myapp
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped
$ ldd myapp
ldd:
not a dynamic executable
$

Donc, pour la commande file</<code>, ce binaire était un exécutable à liaison dynamique, mais pas pour <code>ldd ? Encore plus étonnant, un trivial helloworld.go compilé de la même façon fonctionnait, lui, sans problème sur les deux machines. Que pouvait-il bien se passer ?

Le diagnostic

Voyons ce que dit ldd myapp sur le portable...

ldd usemysql
linux-vdso.so.1 =>  (0x00007fff64fff000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007ff681e8e000)
libc.so.6 => /lib64/libc.so.6 (0x00007ff681b03000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff6820b7000)

Ahah, une piste: le même fichier est identifié différemment par la même commande exécutée sur deux machines différentes. Si je vérifiais ce que contient réellement ce binaire ?

readelf -l usemysql

Elf file type is EXEC (Executable file)
Entry point 0x4258a0
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    1000
  INTERP         0x0000000000000be4 0x0000000000400be4 0x0000000000400be4
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x0000000000111c40 0x0000000000111c40  R E    1000
  LOAD           0x0000000000112000 0x0000000000512000 0x0000000000512000
                 0x0000000000231c50 0x0000000000231c50  R      1000
  LOAD           0x0000000000344000 0x0000000000744000 0x0000000000744000
                 0x0000000000017ac8 0x0000000000035db8  RW     1000
  DYNAMIC        0x00000000003440c0 0x00000000007440c0 0x00000000007440c0
                 0x0000000000000130 0x0000000000000130  RW     8
  TLS            0x000000000035bac8 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000010  R      8
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     8
  LOOS+5041580   0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000         8

Section to Segment mapping:
  Segment Sections...
   00    
   01     .interp
   02     .text .plt .interp
   03     .rodata .typelink .gosymtab .gopclntab .dynsym .rela .gnu.version .gnu.version_r .hash .dynstr .rela.plt
   04     .got .got.plt .dynamic .noptrdata .data .bss .noptrbss
   05     .dynamic
   06     .tbss
   07    
   08    

Donc, malgré les références constantes au caractère statique des binaires Go, ce programme nécessite /lib64/ld-linux-x86-64.so.2 qui existe sur une des machines mais pas sur l'autre. Un lien symbolique vers ld-linux.so.2 depuis le chemin manquant change le tableau (erreur de bibliothèque corrompue), mais ne règle pas le problème.

OK, alors si j'installais Go 1.2.2 sur ce serveur et compilais localement pour voir les différences ?

Le paquet golang-go de Debian Squeeze ne contient que la version Go 1.0, donc ce n'est pas une solution, donc en avant pour les instructions données sur http://golang.org/doc/install... et même erreur pour le binaire go officiel de la distribution ! Bon, ce n'est pas /si/ surprenant, à ce stade: je n'ai plus qu'à reconstruire Go à partir des sources : c'est bien documenté, sur http://golang.org/doc/install/source, donc allons-y.

# cd /opt
# tar xf go1.2.2.linux-amd64.tar.gz
# cd go/src
# ./app.bash
[...snip...]
/usr/include/features.h:324:26: fatal error: bits/predefs.h: No such file or directory
#include <bits/predefs.h>

Ainsi donc, la distribution officielle ne se compile pas non plus. Bon, Squeeze est probablement trop ancien, et il fallait que je mette ce serveur à niveau vers Wheezy de toute façon... et hop un upgrade.

[...sautons la MAJ Squeeze → Wheezy...]
# cd go/src
# ./app.bash
[...snip...]
/usr/include/features.h:324:26: fatal error: bits/predefs.h: No such file or directory
#include <bits/predefs.h>

On ne pas pas dire que ce soit tellement mieux... et le fichier predefs.h qu'il ne trouve pas est pourtant bel et bien présent en /usr/include/i386-linux-gnu/bits/predefs.h. Il était déjà à cet emplacement sur Squeeze, à vrai dire. Donc, pour une raison inconnue, il apparaît que le processus de construction de Go a une dépendance non documentée sur les configurations d'architecture multilib non standard.

La solution

A ce stade, je n'ai plus qu'à l'installer ce soutien multilib, alors même qu'il n'est pas nécessaire sur Ubuntu 12.04 :

# apt-get install gcc-multilib
# cd /opt/go/src
# ./all.bash
[...des tonnes de résultats, y compris des panic dans les tests réseau...]

Enfin, l'installation de go réussit ! Ignorons pour l'instant les tests réseau en échec, qui n'ont pas prévu les particularités IP de cette VM (modifier src/pkg/net/ipraw_test.go et src/pkg/net/http/fs_test.go pour basculer la casse sur les tests en échec), et c'est tout bon:

# go version
go version go1.2.2 linux/amd64
# cd /tmp
# ./myapp
[...les résultats attendus...]
#

En résumé: dans de telles situations de déploiement, plonger dans les entrailles des binaires peut effectivement être utile. La combinaison file, ldd / readelf fait des miracles.

Remerciement tout particuliers à taruti sur #go-nuts, pour m'avoir vraiment mis sur le chemin de la solution.