Recentemente, notei que em diferentes projetos tenho que escrever ativamente operações bit a bit em PHP. Esta é uma habilidade muito interessante e útil que vem a calhar desde a leitura de binários até a emulação de processadores.
O PHP tem muitas ferramentas para ajudá-lo a manipular dados binários, mas quero avisá-lo imediatamente: se você quer uma eficiência de baixo nível, essa linguagem não é para você.
E agora aos negócios! Neste artigo, contarei muitas coisas interessantes sobre operações bit a bit, processamento binário e hexadecimal, que serão úteis em QUALQUER linguagem.
- Por que o PHP pode não ser o melhor candidato
- Uma introdução rápida à representação binária e hexadecimal de dados
- Operações de transferência
- Representação de dados na memória do computador
- Estouros aritméticos
- PHP
- : PHP, ?
- PHP
PHP
Eu amo PHP, não me entenda mal. E tenho certeza de que essa linguagem funcionará bem na maioria dos casos. Mas se você precisa de eficiência máxima no processamento de dados binários, o PHP não o fará.
Deixe-me explicar: não estou falando sobre o fato de que o aplicativo pode consumir cinco ou dez megabytes a mais, mas sobre a alocação de uma quantidade específica de memória para armazenar dados de um determinado tipo.
De acordo com a documentação oficial sobre inteiros , PHP representa valores decimais, hexadecimais, octais e binários usando um tipo inteiro. Portanto, não importa quais dados você coloque lá, eles sempre serão inteiros.
Você provavelmente já conhece o ZVAL - é uma estrutura C que representa cada variável do PHP. Possui um campo zend_long para representar todos os números . Este campo tem um tipo
lval
cujo tamanho depende da plataforma: nas plataformas de 64 bits, o campo será representado como um número de 64 bits e nas plataformas de 32 bits como um número de 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
O resultado final é o seguinte: não importa se você precisa armazenar 0xff, 0xffff, 0xffffff ou qualquer outra coisa. No PHP, todos esses valores serão armazenados como long ( lval ) com um comprimento de 32 ou 64 bits.
Por exemplo, recentemente experimentei emular microcontroladores. E embora fosse necessário lidar com o conteúdo e as operações da memória corretamente, eu não precisava de muita eficiência de memória porque minha máquina de hospedagem estava compensando os custos de ordens de magnitude.
Claro, tudo muda quando falamos sobre extensões C ou FFI, mas este também não é meu objetivo. Estou falando de PHP puro.
Portanto, lembre-se: ele funciona e pode se comportar da maneira que você deseja, mas na maioria dos casos os tipos irão desperdiçar memória de forma ineficiente.
Uma introdução rápida à representação binária e hexadecimal de dados
Antes de falar sobre como o PHP lida com dados binários, você deve primeiro falar sobre o que é binário. Se você acha que já sabe tudo sobre isso, pule para o capítulo Números binários e strings em PHP .
Na matemática, existe o conceito de "fundação". Ele define como podemos representar quantidades em diferentes formatos. As pessoas geralmente usam a base decimal (base 10), o que nos permite representar qualquer número com os dígitos 0, 1, 2, 3, 4, 5, 6, 7, 8 e 9.
Para esclarecer o próximo exemplo, vou me referir ao número 20 como "Decimal 20".
Os números binários (base 2) podem representar qualquer número, mas usando apenas dois dígitos: 0 e 1.
20 decimal em binário tem a seguinte aparência: 0b000 10100 . Você não precisa convertê-lo para sua forma familiar, deixe os computadores fazerem isso. ;)
Números hexadecimais (base 16) podem representar qualquer número usando dez dígitos 0, 1, 2, 3, 4, 5, 6, 7, 8 e 9, bem como seis caracteres adicionais do alfabeto latino: a, b, c , d, e e f.
O 20 decimal na forma hexadecimal tem a seguinte aparência: 0x14. Deixe a transformação para os computadores, eles são especialistas nisso!
É importante entender que os números podem ser representados em diferentes bases: binária (base 2), octal (base 8), decimal (base 10, nosso usual) e hexadecimal (base 16).
Em PHP e em muitas outras linguagens, os números binários são escritos como qualquer outro, mas prefixados com 0b : decimal 20 parece 0b 00010100. Números hexadecimais são prefixados 0x : decimal 20 parece 0x 14.
Como você já deve saber, os computadores não armazenam dados literais ... Eles são todos representados na forma de números binários, zeros e uns. Símbolos, números, letras, instruções - tudo é apresentado na base 2. As letras são apenas uma convenção de sequências numéricas. Por exemplo, a letra "a" é o número 97 na tabela ASCII.
Mas embora tudo seja armazenado em binário, os programadores se sentem mais à vontade para ler os dados em formato hexadecimal. Eles parecem melhores assim. Apenas olhe:
# string "abc"
'abc'
# binary form (bleh)
0b01100001 0b01100010 0b01100011
# hexadecimal form (such wow)
0x61 0x62 0x63
Embora o formato binário ocupe visualmente muito espaço, os dados hexadecimais são muito semelhantes à representação binária. Portanto, geralmente os usamos em programação de baixo nível.
Operações de transferência
Você já está familiarizado com o conceito de transporte, mas tenho que prestar atenção a ele para que possamos usá-lo por diferentes motivos.
No conjunto decimal, temos dez dígitos separados para representar números, de 0 a 9. Mas quando tentamos representar um número maior do que nove, erramos os dígitos! E aqui a operação de transferência é aplicada: prefixamos o número com o dígito 1 e redefinimos o dígito correto para 0.
# decimal (base 10)
1 + 1 = 2
2 + 2 = 4
9 + 1 = 10 // <- Carry
A base binária se comporta da mesma forma, mas é limitada aos dígitos 0 e 1.
# binary (base 2)
0 + 0 = 0
0 + 1 = 1
1 + 1 = 10 // <- Carry
1 + 10 = 11
É o mesmo com a base hexadecimal, só que tem um alcance muito mais amplo.
# hexadecimal (base 16)
1 + 9 = a // no carry, a is in range
1 + a = b
1 + f = 10 // <- Carry
1 + 10 = 11
Como você entendeu, a operação de transporte requer mais dígitos para representar certos números. Isso nos permite entender o quão limitados são determinados tipos de dados e, por serem armazenados em computadores, quão limitada é sua representação binária.
Representação de dados na memória do computador
Como mencionei acima, os computadores armazenam tudo em formato binário. Ou seja, eles contêm apenas zeros e uns na memória.
É mais fácil visualizar este conceito como uma grande tabela com uma linha e muitas colunas (tanto quanto a capacidade de memória permitir. Cada coluna é um número binário (bit). A
representação do nosso decimal 20 em tal tabela usando 8 bits é assim:
| Cargo (endereço) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| Mordeu | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
Um inteiro sem sinal de 8 bits é um número que pode ser representado usando no máximo 8 números binários. Ou seja, 0b11111111 (255 decimal) será o maior número de 8 bits sem sinal. Adicionar 1 a ele exigirá o uso de uma operação de transporte, que não pode mais ser representada com o mesmo número de dígitos.
Sabendo disso, podemos facilmente descobrir por que existem tantas representações na memória para números e o que são: uint8 são inteiros de 8 bits sem sinal (decimal 0-255), uint16 são inteiros de 16 bits sem sinal (decimal 0-65535 ) Existem também uint32, uint64 e, em teoria, superiores.
Inteiros com sinal, que podem representar valores negativos, normalmente usam o último bit para determinar se são positivos (último bit = 0) ou negativos (último bit = 1). Como você pode imaginar, eles permitem que você armazene valores menores na mesma quantidade de memória. Um inteiro de 8 bits com sinal varia de -128 ao 127 decimal.
Aqui está o -20 decimal, representado como um inteiro de 8 bits com sinal. Observe que o primeiro bit está definido (endereço 0, valor 1), isso significa um número negativo.
| Cargo (endereço) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| Mordeu | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
Espero que tudo esteja claro até agora. Esta introdução é essencial para compreender o funcionamento interno dos computadores. Mantenha isso em mente e você sempre entenderá como o PHP funciona nos bastidores.
Estouros aritméticos
A representação do número selecionado (8 bits, 16 bits) determina o valor mínimo e máximo do intervalo. É tudo sobre como os números são armazenados na memória: adicionar 1 ao dígito binário 1 resulta em uma operação de transporte, ou seja, você precisa de outro bit como prefixo para o número atual. Como o formato inteiro é definido com muito cuidado, não podemos confiar em operações de carry fora dos limites (na verdade, é possível, mas muito louco).
| Cargo (endereço) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| Mordeu | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
Aqui, estamos muito próximos do limite de 8 bits (decimal 255). Se adicionarmos um, obtemos 255 decimais em binário:
| Cargo (endereço) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| Mordeu | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Todos os bits são atribuídos! Adicionar 1 exigirá uma operação de transporte que não será possível porque estamos ficando sem bits, todos os 8 já estão atribuídos! Essa situação é chamada de estouro , vamos além de um certo limite. A operação binária 255 + 2 deve dar um resultado 1 de 8 bits.
| Cargo (endereço) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| Mordeu | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Este comportamento não é acidental, o novo valor é calculado usando certas regras, que não vamos considerar aqui.
Números binários e strings em PHP
De volta ao PHP! Desculpe por esta grande digressão, mas acho que é importante.
Espero que você já tenha peças de um quebra-cabeça em sua cabeça: números binários, como eles são armazenados, o que é overflow, como o PHP representa números ...
Decimal 20, representado em PHP como um valor inteiro, pode ter duas representações diferentes dependendo da plataforma ... Na plataforma x86 será uma representação de 32 bits, no x64 será de 64 bits, mas em ambos os casos haverá um sinal (ou seja, o valor pode ser negativo). Sabemos que o decimal 20 pode caber em um espaço de 8 bits, mas o PHP trata qualquer número decimal como 32 ou 64 bits.
O PHP também possui strings binárias que podem ser convertidas para frente e para trás usando as funções empacotar () e desempacotar () .
No PHP, a principal diferença entre strings binárias e números é que strings simplesmente contêm dados, como um buffer. Os valores inteiros (binários e não apenas) permitem que você execute operações aritméticas com eles próprios, mas também valores binários (bit a bit) como AND, OR, XOR e NOT.
Binário: o que deve ser usado em PHP, números ou strings?
Normalmente usamos strings binárias para transportar dados. Portanto, a leitura de um arquivo binário ou rede requer compactação e descompactação de strings binárias.
No entanto, operações reais como OR e XOR não podem ser realizadas de forma confiável com strings, então você precisa usar números.
Depurando valores binários em PHP
Agora vamos nos divertir e brincar com alguns códigos PHP!
Primeiro, mostrarei como visualizar dados. Devemos entender com o que estamos lidando.
Depurar inteiros é muito, muito fácil, podemos usar a função sprintf () . Ele tem uma formatação muito poderosa e nos ajudará a entender rapidamente com quais valores estamos trabalhando.
Vamos representar o decimal 20 em binário de 8 bits e hexadecimal de 1 byte:
<?php
// Decimal 20
$n = 20;
echo sprintf('%08b', $n) . "\n";
echo sprintf('%02X', $n) . "\n";
// Output:
00010100
14
O formato
%08b
produz uma variável em
$n
representação binária (
b
) com oito dígitos (
08
).
O formato
%02X
exibe a variável
$n
em notação hexadecimal (
X
) com dois dígitos (
02
).
Visualizando Strings Binários
Embora em PHP os inteiros tenham sempre 32 ou 64 bits, o comprimento das strings é igual ao comprimento de seu conteúdo. Para decodificar seus valores binários e renderizá-los, precisamos examinar e transformar cada byte.
Felizmente, em PHP, as strings não são nomeadas como matrizes, e cada posição aponta para um caractere de 1 byte. Aqui está um exemplo de símbolos de acesso:
<?php
$str = 'thephp.website';
echo $str[3];
echo $str[4];
echo $str[5];
// Outputs:
php
Supondo que um caractere tenha 1 byte, podemos chamar ord () para converter em um inteiro de 1 byte:
<?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
Agora você pode verificar com o aplicativo de linha de comando hexdump:
$ echo 'php' | hexdump
// Outputs
0000000 70 68 70 ...
A primeira coluna contém apenas o endereço, e na segunda coluna vemos os valores hexadecimais que representam os caracteres
p
,
h
e
p
.
Além disso, ao lidar com strings binárias, podemos usar as funções pack () e unpack () , e eu tenho um ótimo exemplo para você! Digamos que você precise ler um arquivo JPEG para extrair alguns dados (como EXIF). Usando o modo de leitura binária, você pode abrir um gerenciador de arquivos e ler imediatamente os primeiros dois bytes:
<?php
$h = fopen('file.jpeg', 'rb');
// Read 2 bytes
$soi = fread($h, 2);
Para extrair valores em uma matriz de inteiros, você pode simplesmente descompactá-los:
$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
Observe que o formato C na função
unpack()
converte o caractere em uma string
$soi
como números de 8 bits sem sinal. O modificador
*
descompacta a linha inteira.
Operações bit a bit
PHP implementa todas as operações bit a bit de que você pode precisar. Eles são construídos como expressões, e o resultado de seu trabalho é descrito abaixo:
| Código php | Nome | Descrição |
| $ x | $ y | Inclusive OU | $ x e $ y recebem um valor com todos os bits especificados. |
| $ x ^ $ y | Exclusivo ou | $ x ou $ y recebe um valor com os bits fornecidos. |
| $ x e $ y | E | $ x e $ y recebem simultaneamente um valor com os bits fornecidos. |
| ~ $ x | NÃO | Altera os valores de todos os bits em $ x. |
| $ x << $ y | Desvio à esquerda | Desloca os bits de $ x deixados por $ y posições. |
| $ x >> $ y | Deslocamento para a direita | Desloca os bits de $ x para a direita nas posições de $ y. |
Vou explicar como cada um funciona!
Deixe
$x = 0x20
e
$y = 0x30
. Abaixo, mostrarei exemplos usando notação binária.
Como funciona o Inclusive Or ($ x | $ y)
A operação OR inclusiva obtém todos os bits de ambas as entradas. Ou seja, ele
$x | $y
deve retornar
0x30
. Dê uma olhada:
// 1 | 1 = 1
// 1 | 0 = 1
// 0 | 0 = 0
0b00100000 // $x = 0x20
0b00110000 // $y = 0x30
OR ------- // $x | $y
0b00110000 // 0x30
Nota: Da direita para a esquerda, o sexto bit
$x
(1) foi especificado , bem como o quinto e o sexto bits
$y
. Os dados foram reunidos e valor gerado dado o quinto e sexto bits de:
0x30
.
Como funciona o exclusivo Or ($ x ^ $ y)
A operação OR exclusiva (também conhecida como XOR) obtém bits de apenas um lado. Ou seja, o resultado do cálculo
$x ^ $y
será
0x10
:
// 1 ^ 1 = 0
// 1 ^ 0 = 1
// 0 ^ 0 = 0
0b00100000 // $x = 0x20
0b00110000 // $y = 0x30
XOR ------ // $x ^ $y
0b00010000 // 0x10
Como AND ($ x & $ y) funciona
O operador AND é muito mais fácil de entender. Ele aplica uma operação AND a cada bit, de modo que apenas os valores iguais entre si em ambos os lados serão recuperados. O resultado do cálculo
$x & $y
será
0x20
:
// 1 & 1 = 1
// 1 & 0 = 0
// 0 & 0 = 0
0b00100000 // $x = 0x20
0b00110000 // $y = 0x30
AND ------ // $x & $y
0b00100000 // 0x20
Como NÃO (~ $ x) funciona
A operação NOT requer um parâmetro, ela simplesmente altera os valores de todos os bits transmitidos. Ele transforma todos os 0s em 1 e todos os 1s em 0.:
// ~1 = 0
// ~0 = 1
0b00100000 // $x = 0x20
NOT ------ // ~$x
0b11011111 // 0xDF
Se você executou essa operação em PHP e decidiu depurar com
sprintf()
, provavelmente notou números maiores? No capítulo sobre Normalização de Números, explicarei o que está acontecendo aqui e como consertar.
Como SHIFT esquerdo e SHIFT direito funcionam ($ x << $ n e $ x >> $ n)
O deslocamento de bits é semelhante à multiplicação ou divisão de números por uma potência de dois. Todos os bits vão
$n
para as posições esquerda ou direita.
Vamos pegar um pequeno número binário para facilitar a exibição, por exemplo
$x = 0b0010
. Se movermos para a
$x
esquerda uma vez , esse bit deve se mover uma posição para a esquerda:
$x = 0b0010;
$x = $x << 1;
// 0b0100
Mesma coisa com deslocamento à direita:
$x = 0b0100;
$x = $x >> 2;
// 0b0001
Ou seja, deslocar o número de
$n
vezes para a esquerda é equivalente a multiplicar
$n
duas vezes e deslocar o número de
$n
vezes para a direita é equivalente a dividir por dois
$n
.
O que é máscara de bits
Muitas coisas interessantes podem ser feitas com essas operações e outras técnicas. Por exemplo, aplique uma máscara de bits. Este é um número binário arbitrário de sua escolha, criado para extrair informações muito específicas.
Por exemplo, considere que um número com sinal de 8 bits é positivo se o oitavo bit (0) não for especificado e negativo se um bit for especificado. O número é positivo ou negativo
0x20
? Sobre o quê
0x81
?
Para responder a isso, podemos criar um byte muito conveniente com um único bit negativo especificado (
0b10000000
, equivalente
0x80
) e
0x20
executar AND nele. Se o resultado for
0x80
(
0b10000000
, nossa máscara), então este é um número negativo, caso contrário, é positivo:
// 0x80 === 0b10000000 (bitmask)
// 0x20 === 0b00100000
// 0x81 === 0b10000001
0x20 & 0x80 === 0x80 // false
0x81 & 0x80 === 0x80 // true
Isso geralmente é necessário ao trabalhar com sinalizadores. Você pode até encontrar exemplos de uso no próprio PHP, como sinalizadores de mensagem de erro .
Você pode escolher quais tipos de erros serão gerados:
error_reporting(E_WARNING | E_NOTICE);
O que está acontecendo aqui? Basta olhar para o seu significado:
0b00000010 (0x02) E_WARNING
0b00001000 (0x08) E_NOTICE
OR -------
0b00001010 (0x0A)
Quando o PHP vê uma notificação que pode ser enviada, ele verifica algo assim:
// 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
E você vai ver isso em todos os lugares! Binários, processadores, todos os tipos de coisas de baixo nível!
Números normalizados
O PHP tem uma peculiaridade relacionada ao manuseio de números binários: inteiros têm 32 ou 64 bits de tamanho. Isso significa que muitas vezes precisamos normalizá-los para confiar em nossos cálculos.
Por exemplo, executar esta operação em uma máquina de 64 bits dará um resultado estranho (mas esperado):
echo sprintf(
'0b%08b',
~0x20
);
// Expected
0b11011111
// Actual
0b1111111111111111111111111111111111111111111111111111111111011111
O que aconteceu aqui? A operação NOT em um inteiro de 8 bits (
0x20
) transformou todos os bits zero em uns. Adivinha o que temos zeros? Isso mesmo, todos os outros 56 bits da esquerda, que antes eram ignorados!
Novamente, o motivo é que no PHP o comprimento dos inteiros é de 32 ou 64 bits, independentemente de seu valor!
No entanto, o código funciona conforme o esperado. Por exemplo, o resultado da operação ~
0x20 & 0b11011111 === 0b11011111
será um valor booleano (verdadeiro). Mas não se esqueça de que esses bits à esquerda não vão a lugar nenhum, caso contrário, você obterá um comportamento de código estranho.
Para resolver esse problema, você pode normalizar os números aplicando uma máscara de bits que apaga todos os zeros. Por exemplo, para normalizar
~0x20
um inteiro de 8 bits deve ser submetido a AND com
0xFF
(
0b11111111
) para que todos os 56 bits anteriores se tornem zeros.
~0x20 & 0xFF
-> 0b11011111
Atenção! Não se esqueça do que está em suas variáveis, caso contrário, você obterá um comportamento inesperado. Por exemplo, vamos dar uma olhada no que acontece quando deslocamos o valor acima para a direita sem uma máscara de 8 bits:
~0x20 & 0xFF
-> 0b11011111
0b11011111 >> 2
-> 0b00110111 // expected
(~0x20 & 0xFF) >> 2
-> 0b00110111 // expected
(~0x20 >> 2) & 0xFF
-> 0b11110111 // expected?
Deixe-me explicar: do ponto de vista do PHP, isso é esperado, porque você está processando explicitamente um número de 64 bits. Você precisa entender o que o SEU programa está esperando.
Dica: evite esses erros bobos programando no paradigma TDD .
Conclusão: Binário e PHP são legais
Uma vez armado com essas ferramentas, todo o resto se torna apenas encontrar a documentação correta sobre o comportamento de binários ou protocolos. Afinal, tudo são sequências binárias.
Eu recomendo fortemente a leitura das especificações PDF ou EXIF. Você pode até querer experimentar sua própria implementação do formato de serialização MessagePack , ou Avro, Protobuf ... As possibilidades são infinitas!