Les fonctions pack
, unpack
et vec
de Perl
Supplément au cours PLURITAL n°1 du 24 septembre 2013
Révision du 15/10/2013 (merci à Pierre Marchal !).
Du point de vue du cours GIM, ces fonctions permettent d'explorer les
phénomènes de codage et de décodage de l'information.
Pour l'information disponible en mémoire centrale elles jouent un rôle
analogue à celui de l'éditeur hexadécimal pour les fichiers.
Attention ! leur maniement est délicat, et l'absence de typage en Perl,
allié à la plasticité de la syntaxe,
peut conduire à des résultats déroutants.
Pour de plus amples explications, voyez par exemple ce
tutoriel.
Pour un traitement systématique des rapports entre Perl et le binaire,
voyez le chapitre Working with Bits d'un ouvrage au titre ambitieux (Mastering Perl)
paru chez O'Reilly en 2007.
- La fonction pack
- Caractères
- Hexa
- Binaire
- La fonction unpack
- Combinaison des deux
fonctions pack et unpack
- Et les nombres
entiers ?
- Obtenir un entier
par pack
- Attention !
chaînes !
- Conséquence :
pour avoir des entiers...
- Retrouver une
chaîne par unpack d'un entier
- Chaîne
décimale
- Chaîne
binaire ou hexadécimale
- Conversion entre
chaînes représentant le même entier
- À partir d'une
chaîne décimale
- À partir
d'une chaîne hexadécimale
- À partir
d'une chaîne en octal
- À partir
d'une chaîne binaire
- La fonction vec
- Accès aux octets
- Accès aux bits
- Autres formats
-
- prend comme arguments
- une chaîne F dite format
- une liste de valeurs L (chaînes, caractères,
nombres)
- renvoie une suite d'octets réalisant la liste L
- conformément au format défini par la chaîne F.
Attention ! Il est essentiel que les valeurs de la liste L et
le format F soient compatibles...
C'est une observation de bon sens, mais prenez garde qu'en
programmation le bon sens n'est pas toujours facile à discerner. Perl
ne vous enverra que rarement un message d'erreur intelligible...
En outre, la syntaxe des chaînes de format n'est pas totalement banale.
Exemples :
N.B. Les suites d'octets construites par pack
sont
destinées à la
mémoire centrale et ne sont donc que rarement imprimables directement
avec un résultat compréhensible.
Il faut en général faire appel à la fonction inverse unpack
pour les observer.
Nos premiers essais seront donc limités.
-
La chaîne de format
'C'
annonce que la valeur
correspondante dans la liste L représente un caractère (8
bits).
Cette
valeur doit être un entier entre 0 et 255.
my @lc = (77, 109, 68, 100, 69, 101);
pack('C', @lc)
---> la chaîne 'M'
de
longueur 1
pack('CCC', @lc)
---> la chaîne 'MmD'
de longueur 3
pack('C5', @lc)
---> la chaîne 'MmDdE'
de longueur 5
pack('C*', @lc)
---> la chaîne 'MmDdEe'
de longueur égale à celle de la liste @lc
En particulier, on voit que pack('C', $x)
est synonyme de
chr($x)
.
-
La chaîne de format
'H'
annonce que la valeur
correspondante dans la liste représente un entier en hexadécimal.
Cette
valeur doit être un chiffre hexadécimal, et le nombre de chiffres dans
le format doit correspondre à la longueur du nombre : 'H2'
permet de lire un octet.
Pour obtenir le même résultat 'MmDdEe'
que ci-dessus, il
faut partir de
my @lh = ('4d', '6d', '44', '64', '45', '65');
et demander
pack('H2', @lh)
---> 'M'
pack('H2H2', @lh)
---> 'Mm'
pack('H2'x6, @lh)
---> 'MmDdEe'
ou bien de
my $nh = '4d6d44644565';
en demandant pack('H12', $nh)
ou pack('H*', $nh)
---> 'MmDdEe'
-
La chaîne de format
'B'
annonce que la valeur
correspondante dans la liste représente un entier en binaire.
Cette
valeur doit être un chiffre binaire, et le nombre de chiffres dans le
format doit correspondre à la longueur du nombre : 'B8'
permet de lire un octet.
Pour obtenir le même résultat 'MmDdEe'
que ci-dessus, il
faut partir de
my @lb = ('01001101', '01101101', '01000100', '01100100',
'01000101', '01100101');
et demander
pack('B8', @lb)
---> 'M'
pack('B8B8', @lb)
---> 'Mm'
pack('B8'x6, @lb)
---> 'MmDdEe'
ou bien de
my $nb =
'010011010110110101000100011001000100010101100101';
en demandant pack('B48', $nb)
ou pack('B*', $nb)
---> 'MmDdEe'
-
Comme son nom l'indique, elle effectue la transformation inverse de
celle de
pack
.
Elle
- prend comme arguments
- une chaîne F dite format (la même que
celle de
pack
)
- une suite d'octets censée réaliser une liste de
valeurs L (chaînes, caractères,
nombres)
- conformément au format défini par la chaîne F
- renvoie la liste L en question
Exemples inverses de exemples ci-dessus :
my $chn = 'MmDdEe';
unpack('C*', $chn)
---> (77, 109, 68, 100,
69, 101)
unpack('C2', $chn)
---> (77, 109)
et unpack('C', $x)
s'avère synonyme de ord($x)
.
unpack('H*', $chn)
---> (4d6d44644565)
unpack('H2'x2, $chn)
---> (4d, 6d)
unpack('B*', $chn)
---> (010011010110110101000100011001000100010101100101)
unpack('B8', $chn)
---> (01001101)
Attention ! Ces exemples fonctionnent correctement parce que la chaîne est compatible avec les formats employés.
Si cette chaîne comportait des caractères chinois, par exemple, nous aurions des avertissements (incompréhensibles)
comme Character in 'H' format wrapped in unpack at
...
et des résultats complètement faux.
Ceci est dû à la sémantique des chaînes en Perl (chaînes de caractères vs. chaînes d'octets) dont nous parlerons plus tard
dans les cours GIM n°3 et 4.
-
Elle permet d'effectuer différentes sortes de conversions.
- Le nom
$n
(hexa), la valeur $v
(entière, entre 0 et 255) et l'octet $o
lui-même (en
binaire)
$o == unpack('B8', pack('C', $v))
$n == unpack('H2', pack('C', $v))
$v == unpack('C', pack('H2', $n))
$o == unpack('B8', pack('H2', $n))
$v == unpack('C', pack('B8', $o))
$n == unpack('H2', pack('B8', $o))
- Assurer que l'initiale d'une chaîne ASCII (supposée être une
lettre) est une majuscule
(comme la fonction ucfirst
)
sub majInit($){ # arg. chaine
my ($chn) = @_;
my $k = length($chn); #nombre d'octets, $chn est supposée ASCII
my @lc = unpack('B8'x$k, $chn);
my $init = $lc[0]; # chaîne binaire de l'initiale
my @tabin = split(//, $init); # liste des bits de
l'initiale
$tabin[2] = 0; # format "big-endian"
$init = join ('', @tabin);
$lc[0] = $init;
return pack('B8'x$k, @lc);
}
-
-
-
Perl représente ses entiers le plus souvent
comme des
chaînes de
caractères (décimaux) et non pas en binaire conforme à la théorie.
Ne cherchez donc pas à décoder directement des valeurs entières, vous
serez déçu(e)s.
Rappel : Perl connaît 4 formats pour écrire les constantes entières
(attention, pas de
guillemets !)
Par exemple :
$nbd = 8924;
(décimal, sans zéro
initial)
$nbo = 0567;
(octal, avec zéro initial)
$nbh = 0x27B9E;
(hexadécimal)
- et, depuis la version 5.6 de Perl,
$nbb =
0b00100100000000100111101110011110;
ou (équivalent plus lisible) $nbb =
0b00100100__0000_0010__0111_1011__1001_1110;
(entier long, valant 604142494)
Essais de décodage direct :
unpack("H*", $nbh)
---> 313632373138
à savoir les 6 octets représentant les 6 chiffres 162718
,
écriture décimale de 0x27B9E
donnés en hexa.
unpack("C*", $nbh)
---> 495450554956
à savoir les 6 octets représentant les 6 chiffres 162718
,
écriture décimale de 0x27B9E
donnés en décimal.
La valeur entière en question est donc représentée en Perl par la
chaîne 162718
, et non en binaire qui serait
0000 0010 0111 1011 1001 1110
, alias 027B9E
hexa.
Perl a ses raisons pour procéder ainsi, dans lesquelles nous
n'entrerons pas.
-
Pour avoir de "vrais" entiers, il faut les recoder dans un format ad
hoc, comme
- le format
'N'
(majuscule) qui
représente les entiers longs (32 bits) avec l'hypothèse "big-endian",
- ou le format
'n'
(minuscule) qui
représente les entiers courts (16 bits) avec la même hypothèse .
À partir d'une chaîne représentant un nombre en Perl, comme ci-dessus,
on obtient un entier sur 32 bits par pack('N', $nb)
et
sur 16 bits par pack('n', $nb)
.
Naturellement, il faut choisir le format en fonction de la taille du
nombre à représenter, donc
pack('n', $nbo)
, pack('n', $nbd)
,
mais certainement pack('N', $nbh)
et pack('N',
$nbb)
!
Le résultat n'est pas imprimable, même en invoquant un formattage par printf
...
printf("%d\n", $nbo); #375
printf("%d\n", pack('n', $nbo)); #0
-
-
en utilisant
unpack
('n',...)
ou unpack ('N', ...)
suivant le format retenu de
16 ou de 32 bits,
et on obtiendra l'écriture décimale, sans zéros initiaux :
unpack("N", pack("N",
0x27B9E))
---> 162718
unpack("n", pack("n",
0567
))
---> 375
-
unpack
('B*',...)
donne le binaire, et unpack
('H*',...)
donne l'hexa,
avec les zéros initiaux correpondant au format 16 ou 32 bits.
unpack("B*", pack("N",
0x27B9E))
---> 00000000000000100111101110011110
unpack("B*", pack("n",
0567
))
---> 0000000101110111
unpack("H*", pack("N",
0x27B9E))
---> 00027b9e
unpack("H*", pack("n",
0567
))
---> 0177
Si on le souhaite, on pourra éliminer les zéros initiaux, pour
retrouver le même effet que sprintf("%x", $nb)
- mais ceci
est une
autre histoire.
-
-
comme
my $chd ='8924'
(avec les guillemets de chaîne)
on obtient une valeur entière dans n'importe que contexte en lui
appliquant la fonction int()
,
et on est ramené au problème précédent.
Attention ! cette fonction s'attend à recevoir une chaîne décimale,
même commençant par des zéros.
int('0567') == 567
!
La présence de chiffres hexadécimaux soulève une protestation du genre
Argument "0x27B9E" isn't numeric in int...
-
même jeu, mais avec la fonction
hex()
.
Attention également, cette fonction lit en hexadécimal...
hex('0567') == 1383
!
-
idem, avec la fonction
oct()
...
Naturellement, si vous lui donnez une chaîne décimale contenant 8 ou 9,
cette fonction va protester :
Illegal octal digit '9' ignored
En revanche, elle comprend les écritures en hexa et en binaire !
oct('0x27B9E')
---> 162718
oct('0b00100100000000100111101110011110')
---> 604142494
mais bien sûr il ne faut pas oublier les préfices '0x
' et '0b
'...
-
Comme indiqué ci-dessus on peut préfixer '
0b
' à la
chaîne-candidate et appeler la fonction oct()
.
On peut aussi procéder autrement :
- d'abord coder cette chaîne comme une séquence
d'octets via
pack("B...", ...)
,
- ensuite décoder cette séquence via
unpack('n',
...)
ou unpack('N', ...)
.
Mais il faut prendre garde à ce que la séquence produite par pack
soit conforme au format choisi (16 ou 32 bits),
et pour cela il convient
- d'utiliser
pack("B16", ...)
ou pack("B32",
...)
selon le cas
- et de veiller à ce que la chaîne-candidate ait la
bonne longueur, avec le nombre de zéros initiaux nécessaire -
suivant par exemple la recette que voici, empruntée à ce cookbook pour cadrer sur 32 bits
une chaîne $chb
de 0 et de 1 : substr('0' x 32 .
$chb, -32)
Dès lors pack("B32", substr("0" x 32 . $chb,
-32))
nous
livre la suite de 4 octets correspondante, et unpack("N",
pack("B32", substr("0" x 32 . $chb, -32)))
renvoie l'entier, en décimal.
- Pour
$chb =
'00000000000000100111101110011110'
(ne
pas oublier les guillemets !) on obtient 162718,
qui est bien l'écriture décimale de 0x27B9E.
- Avec
$chb =
'0111101110011110'
,
unpack("n", pack("B16", substr("0" x 16 . $chb, -16)))
vaut 375
qui correspond à 0567
en base 8.
On peut aussi décoder la séquence d'octets produite par pack avec unpack('H*',
...)
pour
obtenir de l'l'hexadécimal. Dans ce cas, la contrainte sur le format de
la chaîne binaire candidate est moins stricte : il suffit de s'assurer
que sa longueur est multiple de 8, avec le nombre de zéros initiaux
nécessaire.
- Pour
$chb =
'000000100111101110011110'
(avec
un octet initial de moins que ci-dessus)
unpack("H*", pack("B*", $chb))
donne la
chaîne 027b9e
.
-
Elle permet d'accéder en
lecture et en écriture,
individuellement, aux octets qui composent une chaîne,
et même aux bits eux-mêmes !
On peut ainsi effectuer des opérations "chirurgicales" au niveau le
plus intime de l'information.
N.B. La sémantique des chaînes évoquée précédemment (au sujet de unpack()
) ne concerne pas la fonction vec()
,
qui travaille sur les octets, quel que soit le codage des caractères en vigueur.
-
Étant donné une
chaîne
$chn
, et un entier k
l'expression "vec ($chn, k, 8)"
désigne le
k-ième
octet de $chn
,
donné par sa valeur entière.
Exemple : $chn = 'MmDdEe';
vec ($chn, 0, 8)
---> 77
, vec ($chn, 5,
8)
---> 101
, vec
($chn, 3, 8)
---> 100
.
et l'affectation
vec
($chn, 2, 8) = 97;
a pour effet de
modifier in situ la chaîne 'MmDdEe'
en 'MmadEe'
.
(in
situ, c'est-à-dire que la chaîne
modifiée se trouve à la même
adresse en mémoire,
au contraire de ce que produirait l'affectation $chn = 'MmadEe';
qui provoquerait la création d'une nouvelle chaîne, à une autre
adresse.)
Application : une
fonction qui échange majuscules et minuscules dans
une chaîne ASCII.
sub altMajMin8($){
my ($str) = @_;
my $k = length($str); #longueur en octets
for( my $i = 0; $i < $k; $i++ ){
my $n = vec($str, $i, 8);
if( ($n > 96) && ($n
< 123) ){ #minuscule
$n -= 32;
}elsif( ($n > 64) &&
($n < 91) ){ #majuscule
$n += 32;
}
vec($str, $i, 8) = $n;
}#for
return $str;
}#altMajMin8
altMajMin8('Just Another Perl Hacker,')
---> 'jUST
aNOTHER pERL hACKER,'
N.B.1 : Le caractère in situ de la transformation ne joue ici
aucun rôle, puisqu'en raison de l'appel par valeur
la fonction travaille sur une copie de son argument.
N.B.2 : Dans ce petit programme, l'emploi de la fonction length()
pour obtenir le nombre d'octets de la chaîne-argument
n'est valable que si le mode "utf8" n'est pas activé (par use utf8;
).
Dans le cas contraire, et si la chaîne traitée n'est pas en ASCII, il faut demander d'abord use Encode;
,
et appeler ensuite length(encode_utf8($str))
, ou encore length(Encode::_utf8_off($str))
.
C'est à nouveau la sémantique des chaînes de Perl qui est en cause...
-
Le troisième argument
de la fonction vec doit prendre comme valeur une
puissance de 2 : 1, 2, 4, 8, 16, 32.
Nous venons de voir la signification de la valeur 8 : on découpe la
chaîne (1er arg.) en blocs de 8 bits, et le deuxième argument repère le
bloc choisi, dans l'ordre de gauche à droite.
Les autres les valeurs signifient aussi : on découpe la chaîne en blocs
de n bits, et le deuxième argument repère le bloc choisi, mais
dans un ordre plus compliqué.
Voyons le cas de
vec ($chn, k, 1)
, où les blocs sont les bits.
avec $chn = 'MmDdEe'
c'est à dire '01001101', '01101101', '01000100', '01100100',
'01000101', '01100101'.
vec
($chn, 0, 1)
désigne le dernier
bit du premier octet ---> 1
vec
($chn, 7, 1)
désigne le
premier bit du premier octet ---> 0
vec
($chn, 9, 1)
désigne
l'avant-dernier bit du deuxième octet ---> 0
vec
($chn, 14, 1)
désigne le
deuxième bit du deuxième octet ---> 1
et en général,
vec ($chn, p+8q, 1)
, avec p < 8, désigne le p-ième
bit du q-ième octet lu de droite à gauche.
La transformation majInit()
, synonyme de ucfirst()
, que nous avons laborieusement programmée avec pack
et unpack
,
se réalise en une seule instruction : vec ($chn, 5, 1) = 0;
Application : autre réalisation de la fonction ci-dessus.
sub altMajMin1($){
my ($str) = @_;
my $k = length($str); #longueur en octets
for( my $i = 0; $i < $k; $i++ ){
my $n = vec($str, $i, 8);
if( ($n > 96) && ($n
< 123) ){ #minuscule
vec($str,
8*$i+5, 1) = 0;
}elsif( ($n > 64) &&
($n < 91) ){ #majuscule
vec($str,
8*$i+5, 1) = 1;
}
}#for
return $str;
}#altMajMin1
altMajMin1('Just Another Perl Hacker,')
---> 'jUST
aNOTHER pERL hACKER,'
-
Comme on l'a dit, le
troisième argument de la fonction
vec
règle la taille des blocs du découpage,
et le deuxième désigne lequel de ces bloc est visé. Il s'agit de savoir
comment se fait cette désignation.
- Si la taille
est 8, les blocs sont les octets, énumérés
de gauche à droite,
et les valeurs de la fonction sont leurs valeurs numériques.
- si la taille
est inférieure à 8 (1, 2 ou 4), les blocs
sont repérés de droite à gauche dans les octets énumérés de gauche à
droite :
- on a vu
ci-dessus le cas de la taille 1, où les
blocs sont les bits eux-mêmes ;
- en taille
2, donc 4 blocs par octet, le n° p+4q
(p < 4)
désigne le p-ième bloc de 2 bits (de droite à gauche) dans
le q-ième octet ;
- en taille
4, donc 2 blocs par octet, le n° p+2q
(p = 0 ou 1)
désigne le dernier (resp. le premier) bloc de 4 bits dans le q-ième
octet.
Exercice : écrivez deux versions de la fonction altMajMin()
en tailles 2 et 4.
Ici aussi, les valeurs de la fonction sont les valeurs numériques des
blocs binaires.
- Si la taille
est supérieure à 8 (16 ou 32), on retrouve
la numérotation des blocs de gauche à droite,
et les valeurs sont les entiers obtenus en leur appliquant selon le cas
pack('n',...)
ou pack('N',...)
.
On peut donc examiner la structure de ces blocs en appelant unpack
avec le format idoine, modifier cette structure ad libitum, la pack
er de nouveau et réaffecter l'entier pour modifier la chaîne in situ...
Exemples :
- en taille
16, avec
$chn = 'MmDdEe'
vec ($chn, 0, 16)
---> 19821
vec ($chn, 1, 16)
---> 17508
vec ($chn, 2, 16)
---> 17765
- en taille
32, avec
$chn = 'Just Another Perl
Hacker'
vec ($chn, 0, 32)
--->
1249211252
vec ($chn, 5, 16)
---> 1667982706