Opérations binaires et binaires en PHP



J'ai récemment remarqué que dans différents projets, je dois écrire activement des opérations bit à bit en PHP. C'est une compétence très intéressante et utile qui est utile de la lecture de binaires à l'émulation de processeurs.



PHP dispose de nombreux outils pour vous aider à manipuler des données binaires, mais je tiens à vous avertir tout de suite: si vous voulez une efficacité de très bas niveau, alors ce langage n'est pas pour vous.



Et maintenant aux affaires! Dans cet article, je vais vous dire beaucoup de choses intéressantes sur les opérations binaires, le traitement binaire et hexadécimal, qui seront utiles dans N'IMPORTE QUELLE langue.





PHP



J'adore PHP, ne vous méprenez pas. Et je suis sûr que cette langue fonctionnera très bien dans la plupart des cas. Mais si vous avez besoin d'une efficacité maximale dans le traitement des données binaires, PHP ne le fera pas.



Laissez-moi vous expliquer: je ne parle pas du fait que l'application peut consommer cinq ou dix mégaoctets de plus, mais d'allouer une quantité spécifique de mémoire pour stocker des données d'un certain type.



Selon la documentation officielle sur les entiers , PHP représente des valeurs décimales, hexadécimales, octales et binaires en utilisant un type entier. Peu importe les données que vous y mettez, ce seront toujours des entiers.



Vous connaissez probablement déjà ZVAL - c'est une structure C qui représente chaque variable PHP. Il a un champ zend_long pour représenter tous les nombres . Ce champ a un type lval



dont la taille dépend de la plate-forme: sur les plates-formes 64 bits, le champ sera représenté sous forme de nombre 64 bits , et sur les plates-formes 32 bits, sous forme de nombre 32 bits .



# zval stores every integer as a lval
typedef union _zend_value {
  zend_long lval;
  // ...
} zend_value;

# lval is a 32 or 64-bit integer
#ifdef ZEND_ENABLE_ZVAL_LONG64
 typedef int64_t zend_long;
 // ...
#else
 typedef int32_t zend_long;
 // ...
#endif

      
      





L'essentiel est le suivant: peu importe si vous devez stocker 0xff, 0xffff, 0xffffff ou autre chose. En PHP, toutes ces valeurs seront stockées sous forme de long ( lval ) avec une longueur de 32 ou 64 bits.



Par exemple, j'ai récemment expérimenté l'émulation de microcontrôleurs. Et s'il était nécessaire de gérer correctement le contenu et les opérations de la mémoire, je n'avais pas besoin d'une trop grande efficacité de la mémoire car ma machine d'hébergement compensait des coûts de grande ampleur.



Bien sûr, tout change quand on parle d'extensions C ou FFI, mais ce n'est pas non plus mon objectif. Je parle de PHP pur.



Alors rappelez-vous: cela fonctionne et peut se comporter comme vous le souhaitez, mais dans la plupart des cas, les types gaspilleront la mémoire de manière inefficace.



Une introduction rapide à la représentation binaire et hexadécimale des données



Avant de parler de la façon dont PHP gère les données binaires, vous devez d'abord parler de ce qu'est le binaire. Si vous pensez que vous savez déjà tout à ce sujet, passez au chapitre Nombres binaires et chaînes en PHP .



En mathématiques, il y a le concept de «fondation». Il définit comment nous pouvons représenter des quantités dans différents formats. Les gens utilisent généralement la base décimale (base 10), qui nous permet de représenter n'importe quel nombre avec les chiffres 0, 1, 2, 3, 4, 5, 6, 7, 8 et 9.



Pour clarifier l'exemple suivant, je ferai référence au nombre 20 comme "Décimal 20".



Les nombres binaires (base 2) peuvent représenter n'importe quel nombre, mais en utilisant uniquement deux chiffres: 0 et 1.



Le décimal 20 en binaire ressemble à ceci: 0b000 10100 . Vous n'avez pas besoin de le convertir vous-même dans sa forme familière, laissez les ordinateurs le faire. ;)



Les nombres hexadécimaux (base 16) peuvent représenter n'importe quel nombre en utilisant dix chiffres 0, 1, 2, 3, 4, 5, 6, 7, 8 et 9, ainsi que six caractères supplémentaires de l'alphabet latin: a, b, c , d, e et f.



Le décimal 20 sous forme hexadécimale ressemble à ceci: 0x14. Laissez la transformation aux ordinateurs, ils sont experts en la matière!



Il est important de comprendre que les nombres peuvent être représentés dans différentes bases: binaire (base 2), octale (base 8), décimale (base 10, notre habitude) et hexadécimale (base 16).



En PHP et dans de nombreux autres langages, les nombres binaires sont écrits comme les autres, mais préfixés par 0b : le décimal 20 ressemble à 0b 00010100. Les nombres hexadécimaux sont préfixés par 0x : le décimal 20 ressemble à 0x 14.



Comme vous le savez peut-être déjà, les ordinateurs ne stockent pas de données littérales ... Ils sont tous représentés sous forme de nombres binaires, de zéros et de uns. Symboles, chiffres, lettres, instructions - tout est présenté en base 2. Les lettres ne sont qu'une convention de séquences de nombres. Par exemple, la lettre «a» est numérotée 97 dans la table ASCII.



Mais alors que tout est stocké en binaire, les programmeurs sont plus à l'aise pour lire les données au format hexadécimal. Ils sont plus beaux ainsi. Il suffit de regarder:



# string "abc"
'abc'

# binary form (bleh)
0b01100001 0b01100010 0b01100011

# hexadecimal form (such wow)
0x61 0x62 0x63

      
      





Bien que le format binaire occupe visuellement beaucoup d'espace, les données hexadécimales sont très similaires à la représentation binaire. Par conséquent, nous les utilisons généralement dans la programmation de bas niveau.



Opérations de transfert



Vous connaissez déjà le concept de carry, mais je dois y prêter attention afin que nous puissions l'utiliser pour différentes raisons.



Dans l'ensemble décimal, nous avons dix chiffres distincts pour représenter les nombres, de 0 à 9. Mais lorsque nous essayons de représenter un nombre supérieur à neuf, nous manquons les chiffres! Et ici, l'opération de transfert est appliquée: nous préfixons le numéro avec le chiffre 1, et remettons le chiffre de droite à 0.



# decimal (base 10)
1 + 1 = 2
2 + 2 = 4
9 + 1 = 10 // <- Carry

      
      





La base binaire se comporte de la même manière, seulement elle est limitée aux chiffres 0 et 1.



# binary (base 2)
0 + 0  = 0
0 + 1  = 1
1 + 1  = 10 // <- Carry
1 + 10 = 11

      
      





C'est la même chose avec la base hexadécimale, mais elle a une portée beaucoup plus large.



# hexadecimal (base 16)
1 + 9  = a // no carry, a is in range
1 + a  = b
1 + f  = 10 // <- Carry
1 + 10 = 11

      
      





Comme vous l'avez compris, l'opération de report nécessite plus de chiffres pour représenter certains nombres. Cela nous permet de comprendre à quel point certains types de données sont limités et, puisqu'elles sont stockées dans des ordinateurs, à quel point leur représentation binaire est limitée.



Représentation des données dans la mémoire de l'ordinateur



Comme je l'ai mentionné ci-dessus, les ordinateurs stockent tout au format binaire. Autrement dit, ils ne contiennent que des zéros et des uns en mémoire.



Le moyen le plus simple de visualiser ce concept est sous la forme d'une grande table avec une ligne et plusieurs colonnes (autant que la capacité de mémoire le permet. Chaque colonne est un nombre binaire (bit). La



représentation de notre décimal 20 dans un tel tableau utilisant 8 bits ressemble à ceci:



Poste (adresse) 0 une 2 3 4 5 6 sept
Bit 0 0 0 une 0 une 0 0


Un entier 8 bits non signé est un nombre qui peut être représenté en utilisant un maximum de 8 nombres binaires. Autrement dit, 0b11111111 (255 décimal) sera le plus grand nombre de 8 bits non signé. Ajouter 1 à celui-ci nécessitera l'utilisation d'une opération de report, qui ne peut plus être représentée en utilisant le même nombre de chiffres.



Sachant cela, nous pouvons facilement comprendre pourquoi il y a tant de représentations en mémoire pour les nombres et ce qu'elles sont: uint8 sont des entiers 8 bits non signés (décimal 0-255), uint16 sont des entiers 16 bits non signés (décimal 0-65535 ). Il existe également uint32, uint64 et, en théorie, des versions supérieures.



Les entiers signés, qui peuvent représenter des valeurs négatives, utilisent généralement le dernier bit pour déterminer s'ils sont positifs (dernier bit = 0) ou négatifs (dernier bit = 1). Comme vous pouvez l'imaginer, ils vous permettent de stocker des valeurs plus petites dans la même quantité de mémoire. Un entier 8 bits signé va de -128 à 127 décimal.



Voici le décimal -20, représenté sous forme d'entier 8 bits signé. Notez que le premier bit est défini (adresse 0, valeur 1), cela signifie un nombre négatif.



Poste (adresse) 0 une 2 3 4 5 6 sept
Bit une 0 0 une 0 une 0 0


J'espère que tout est clair jusqu'à présent. Cette introduction est essentielle pour comprendre le fonctionnement interne des ordinateurs. Gardez cela à l'esprit, et vous comprendrez toujours comment PHP fonctionne sous le capot.



Débordements arithmétiques



La représentation numérique sélectionnée (8 bits, 16 bits) détermine la valeur minimale et maximale de la plage. Tout dépend de la façon dont les nombres sont stockés en mémoire: ajouter 1 au chiffre binaire 1 conduit à une opération de report, c'est-à-dire que vous avez besoin d'un autre bit comme préfixe pour le nombre actuel. Étant donné que le format entier est très soigneusement défini, nous ne pouvons pas nous fier à des opérations de portage hors limites (en fait possible, mais assez folles).



Poste (adresse) 0 une 2 3 4 5 6 sept
Bit une une une une une une une 0


Nous sommes ici très proches de la limite de 8 bits (255 décimales). Si nous en ajoutons un, nous obtenons 255 décimales en binaire:



Poste (adresse) 0 une 2 3 4 5 6 sept
Bit une une une une une une une une


Tous les bits sont attribués! L'ajout de 1 nécessitera une opération de report qui ne sera pas possible car nous manquons de bits, les 8 sont déjà affectés! Cette situation s'appelle le débordement , on dépasse une certaine limite. L'opération binaire 255 + 2 doit donner un résultat 8 bits de 1.



Poste (adresse) 0 une 2 3 4 5 6 sept
Bit 0 0 0 0 0 0 0 une


Ce comportement n'est pas accidentel, la nouvelle valeur est calculée à l'aide de certaines règles, que nous ne considérerons pas ici.



Nombres binaires et chaînes en PHP



Revenez à PHP! Désolé pour cette grande digression, mais je pense que c'est important.



J'espère que vous avez déjà des pièces de puzzle en tête: les nombres binaires, comment ils sont stockés, qu'est-ce que le débordement, comment PHP représente-t-il les nombres ... Le



décimal 20, représenté en PHP sous forme de valeur entière, peut avoir deux représentations différentes selon la plateforme ... Sur la plate-forme x86, ce sera une représentation 32 bits, sur le x64, ce sera 64 bits, mais dans les deux cas, il y aura un signe (c'est-à-dire que la valeur peut être négative). Nous savons que le nombre décimal 20 peut tenir dans un espace de 8 bits, mais PHP traite tout nombre décimal comme 32 ou 64 bits.



PHP a également des chaînes binaires qui peuvent être converties d'avant en arrière en utilisant les fonctions pack () et décompresser () .



En PHP, la principale différence entre les chaînes binaires et les nombres est que les chaînes contiennent simplement des données, comme un tampon. Les valeurs entières (binaires et pas seulement) vous permettent d'effectuer des opérations arithmétiques avec elles-mêmes, mais également des valeurs binaires (au niveau du bit) telles que AND, OR, XOR et NOT.



Binaire: que faut-il utiliser en PHP, des nombres ou des chaînes?



Nous utilisons généralement des chaînes binaires pour transporter des données. Par conséquent, la lecture d'un fichier binaire ou d'un réseau nécessite le conditionnement et le décompression des chaînes binaires.



Cependant, les opérations réelles telles que OR et XOR ne peuvent pas être effectuées de manière fiable avec des chaînes, vous devez donc utiliser des nombres.



Débogage des valeurs binaires en PHP



Maintenant, amusons-nous et jouons avec du code PHP!



Tout d'abord, je vais vous montrer comment visualiser les données. Nous devons comprendre de quoi nous avons affaire.



Le débogage des entiers est très, très simple, nous pouvons utiliser la fonction sprintf () . Il a un formatage très puissant et nous aidera à comprendre rapidement avec quelles valeurs nous travaillons.



Représentons le décimal 20 en binaire 8 bits et 1 octet hexadécimal:



<?php
// Decimal 20
$n = 20;

echo sprintf('%08b', $n) . "\n";
echo sprintf('%02X', $n) . "\n";

// Output:
00010100
14

      
      





Le format %08b



génère une variable en $n



représentation binaire ( b



) avec huit chiffres ( 08



).



Le format %02X



affiche la variable $n



en notation hexadécimale ( X



) avec deux chiffres ( 02



).



Visualisation des chaînes binaires



Bien qu'en PHP les entiers aient toujours une longueur de 32 ou 64 bits, la longueur des chaînes est égale à la longueur de leur contenu. Pour décoder leurs valeurs binaires et les rendre, nous devons examiner et transformer chaque octet.



Heureusement, en PHP, les chaînes ne sont pas nommées comme des tableaux, et chaque position pointe vers un caractère de 1 octet. Voici un exemple d'accès aux symboles:



<?php
$str = 'thephp.website';

echo $str[3];
echo $str[4];
echo $str[5];

// Outputs:
php

      
      





En supposant qu'un caractère est de 1 octet, nous pouvons appeler ord () pour convertir en un entier de 1 octet:



<?php
$str = 'thephp.website';

$f = ord($str[3]);
$s = ord($str[4]);
$t = ord($str[5]);

echo sprintf(
  '%02X %02X %02X',
  $f,
  $s,
  $t,
);
// Outputs:
70 68 70

      
      





Vous pouvez maintenant vérifier avec l'application de ligne de commande hexdump:



$ echo 'php' | hexdump
// Outputs
0000000 70 68 70 ...

      
      





La première colonne contient uniquement l'adresse, et dans la deuxième colonne, nous voyons les valeurs hexadécimales représentant les caractères p



, h



et p



.



Aussi lors de la gestion des chaînes binaires, nous pouvons utiliser les fonctions pack () et unpack () , et j'ai un excellent exemple pour vous! Disons que vous devez lire un fichier JPEG pour extraire certaines données (comme EXIF). En utilisant le mode de lecture binaire, vous pouvez ouvrir un gestionnaire de fichiers et lire immédiatement les deux premiers octets:



<?php

$h = fopen('file.jpeg', 'rb');

// Read 2 bytes
$soi = fread($h, 2);

      
      





Pour extraire des valeurs dans un tableau d'entiers, vous pouvez simplement les décompresser:



$ints = unpack('C*', $soi);

var_dump($ints);
// Outputs
array(2) {
  [1] => int(-1)
  [2] => int(-40)
}

echo sprintf('%02X', $ints[1]);
echo sprintf('%02X', $ints[2]);
// Outputs
FFD8

      
      





Notez que le format C de la fonction unpack()



convertit le caractère en une chaîne $soi



sous forme de nombres 8 bits non signés. Le modificateur *



décompresse la ligne entière.



Opérations au niveau du bit



PHP implémente toutes les opérations au niveau du bit dont vous pourriez avoir besoin. Ils sont construits en tant qu'expressions et le résultat de leur travail est décrit ci-dessous:



Code php Nom La description
$ x | $ y OU inclusif $ x et $ y reçoivent une valeur avec tous les bits donnés.
$ x ^ $ y OU exclusif $ x ou $ y reçoit une valeur avec les bits donnés.
$ x et $ y ET $ x et $ y reçoivent simultanément une valeur avec les bits donnés.
~ $ x NE PAS Modifiez les valeurs de tous les bits dans $ x.
$ x << $ y Décalage à gauche Décale les bits de $ x vers la gauche de $ y positions.
$ x >> $ y SHIFT droit Décale les bits de $ x vers la droite de $ y positions.


Je vais vous expliquer comment chacun fonctionne!



Laissez $x = 0x20



et $y = 0x30



. Ci-dessous, je vais montrer des exemples utilisant la notation binaire.



Fonctionnement de la méthode inclusive ou ($ x | $ y)



L'opération OU inclusive prend tous les bits des deux entrées. Autrement dit, il $x | $y



devrait revenir 0x30



. Regarde:



// 1 | 1 = 1
// 1 | 0 = 1
// 0 | 0 = 0

0b00100000 // $x = 0x20
0b00110000 // $y = 0x30
OR ------- // $x | $y
0b00110000 // 0x30

      
      





Remarque: De droite à gauche, le sixième bit $x



(1) a été spécifié , ainsi que les cinquième et sixième bits $y



. Les données ont été mises en commun et de la valeur générée étant donné la cinquième et sixième des bits: 0x30



.



Fonctionnement de Exclusive Or ($ x ^ $ y)



L'opération OU exclusif (également appelée XOR) prend des bits d'un seul côté. Autrement dit, le résultat du calcul $x ^ $y



sera 0x10



:



// 1 ^ 1 = 0
// 1 ^ 0 = 1
// 0 ^ 0 = 0

0b00100000 // $x = 0x20
0b00110000 // $y = 0x30
XOR ------ // $x ^ $y
0b00010000 // 0x10

      
      





Comment fonctionne AND ($ x & $ y)



L'opérateur AND est beaucoup plus facile à comprendre. Il applique une opération ET à chaque bit, de sorte que seules les valeurs qui sont égales les unes aux autres des deux côtés seront récupérées. Le résultat du calcul $x & $y



sera 0x20



:



// 1 & 1 = 1
// 1 & 0 = 0
// 0 & 0 = 0

0b00100000 // $x = 0x20
0b00110000 // $y = 0x30
AND ------ // $x & $y
0b00100000 // 0x20

      
      





Comment fonctionne NOT (~ $ x)



L'opération NOT nécessite un paramètre, elle change simplement les valeurs de tous les bits transmis. Il transforme tous les 0 en 1 et tous les 1 en 0.:



// ~1 = 0
// ~0 = 1

0b00100000 // $x = 0x20
NOT ------ // ~$x
0b11011111 // 0xDF

      
      





Si vous avez effectué cette opération en PHP et avez décidé de déboguer avec sprintf()



, vous avez probablement remarqué des nombres plus importants? Dans le chapitre sur la normalisation des nombres, je vais vous expliquer ce qui se passe ici et comment y remédier.



Fonctionnement de SHIFT gauche et droit SHIFT ($ x << $ n et $ x >> $ n)



Le décalage de bits est similaire à la multiplication ou à la division des nombres par une puissance de deux. Tous les bits vont à $n



gauche ou à droite.



Prenons un petit nombre binaire pour le rendre plus facile à afficher, par exemple $x = 0b0010



. Si nous décalons une fois à $x



gauche, ce bit doit se déplacer d'une position vers la gauche:



$x = 0b0010;
$x = $x << 1;
// 0b0100

      
      





Même chose avec le décalage vers la droite:



$x = 0b0100;
$x = $x >> 2;
// 0b0001

      
      





Autrement dit, déplacer le nombre de $n



fois vers la gauche équivaut à multiplier $n



deux fois, et déplacer le nombre de $n



fois vers la droite équivaut à diviser par deux $n



.



Qu'est-ce que le masque de bits



Beaucoup de choses intéressantes peuvent être faites avec ces opérations et d'autres techniques. Par exemple, appliquez un masque de bits. Il s'agit d'un nombre binaire arbitraire de votre choix, créé pour extraire des informations très spécifiques.



Par exemple, prenons l'idée qu'un nombre signé de 8 bits est positif si le huitième bit (0) n'est pas spécifié et négatif si un bit est spécifié. Le nombre est-il positif ou négatif 0x20



? Et quoi 0x81



?



Pour répondre à cela, nous pouvons créer un octet très pratique avec un seul bit négatif spécifié ( 0b10000000



, équivalent 0x80



) et 0x20



AND it. Si le résultat est 0x80



( 0b10000000



, notre masque), alors c'est un nombre négatif, sinon c'est positif:



// 0x80 === 0b10000000 (bitmask)
// 0x20 === 0b00100000
// 0x81 === 0b10000001

0x20 & 0x80 === 0x80 // false
0x81 & 0x80 === 0x80 // true

      
      





Ceci est souvent nécessaire lorsque vous travaillez avec des indicateurs. Vous pouvez même trouver des exemples d'utilisation dans PHP lui-même, comme des indicateurs de message d'erreur .



Vous pouvez choisir le type d'erreurs générées:



error_reporting(E_WARNING | E_NOTICE);

      
      





Que se passe t-il ici? Regardez simplement votre signification:



0b00000010 (0x02) E_WARNING
0b00001000 (0x08) E_NOTICE
OR -------
0b00001010 (0x0A)

      
      





Lorsque PHP voit une notification qui peut être envoyée, il vérifie quelque chose comme ceci:



// error reporting we set before
$e_level = 0x0A;

// Needs to throw a notice
if ($e_level & E_NOTICE === E_NOTICE)
 // Flag is set: throws notice

      
      





Et vous le verrez partout! Binaires, processeurs, toutes sortes de choses de bas niveau!



Normaliser les nombres



PHP a une particularité liée à la gestion des nombres binaires: les entiers ont une taille de 32 ou 64 bits. Cela signifie que nous devons souvent les normaliser afin de nous fier à nos calculs.



Par exemple, exécuter cette opération sur une machine 64 bits donnera un résultat étrange (mais attendu):



echo sprintf(
  '0b%08b',
  ~0x20
);

// Expected
0b11011111
// Actual
0b1111111111111111111111111111111111111111111111111111111111011111

      
      





Que s'est-il passé ici? L'opération NOT sur un entier de 8 bits ( 0x20



) a transformé tous les bits de zéro en uns. Devinez ce que nous avions des zéros? C'est vrai, tous les 56 autres bits à gauche, qui étaient auparavant ignorés!



Encore une fois, la raison est qu'en PHP la longueur des entiers est de 32 ou 64 bits, quelle que soit leur valeur!



Cependant, le code fonctionne comme prévu. Par exemple, le résultat de l'opération ~ 0x20 & 0b11011111 === 0b11011111



sera une valeur booléenne (true). Mais n'oubliez pas que ces bits à gauche ne vont nulle part, sinon vous obtiendrez un comportement de code étrange.



Pour résoudre ce problème, vous pouvez normaliser les nombres en appliquant un masque de bits qui efface tous les zéros. Par exemple, pour normaliser ~0x20



un entier de 8 bits doit être ET avec 0xFF



( 0b11111111



) pour que tous les 56 bits précédents deviennent des zéros.



~0x20 & 0xFF
-> 0b11011111

      
      





Attention! N'oubliez pas ce que contiennent vos variables, sinon vous obtiendrez un comportement inattendu. Par exemple, examinons ce qui se passe lorsque nous décalons la valeur ci-dessus vers la droite sans masque 8 bits:



~0x20 & 0xFF
-> 0b11011111

0b11011111 >> 2
-> 0b00110111 // expected

(~0x20 & 0xFF) >> 2
-> 0b00110111 // expected

(~0x20 >> 2) & 0xFF
-> 0b11110111 // expected?

      
      





Laissez-moi vous expliquer: d'un point de vue PHP, c'est normal, car vous traitez explicitement un nombre 64 bits. Vous devez comprendre ce que VOTRE programme attend.



Astuce: évitez ces erreurs stupides en programmant dans le paradigme TDD .



Conclusion: le binaire et PHP sont cool



Une fois armés de tels outils, tout le reste devient simplement la recherche de la documentation correcte sur le comportement des binaires ou des protocoles. Après tout, tout est des séquences binaires.



Je recommande vivement de lire les spécifications PDF ou EXIF. Vous voudrez peut-être même expérimenter votre propre implémentation du format de sérialisation MessagePack , ou Avro, Protobuf ... Les possibilités sont infinies!



All Articles