PHPでのバイナリおよびビット単位の操作



最近、さまざまなプロジェクトで、PHPでビット単位の操作を積極的に作成する必要があることに気付きました。これは、バイナリの読み取りからプロセッサのエミュレートまで、非常に興味深く便利なスキルです。



PHPには、バイナリデータの操作に役立つツールがたくさんありますが、すぐに警告したいと思います。超低レベルの効率が必要な場合、この言語は適していません。



そして今、ビジネスに!この記事では、ビット単位の操作、バイナリおよび16進処理について多くの興味深いことを説明します。これらは、どの言語でも役立ちます。





PHP



私はPHPが大好きです、誤解しないでください。そして、私はこの言語がほとんどの場合うまくいくと確信しています。ただし、バイナリデータの処理で最大の効率が必要な場合、PHPはそれを行いません。



説明させてください。アプリケーションが5メガバイトまたは10メガバイト以上を消費する可能性があるという事実についてではなく、特定のタイプのデータを格納するために特定の量のメモリを割り当てることについて話します。 整数に関する公式ドキュメントに



よると 、PHPは整数型を使用して10進、16進、8進、および2進の値を表します。したがって、そこにどのデータを配置するかは関係ありません。常に整数になります。



ZVALについてはすでにご存知でしょう。これは各PHP変数を表すC構造です。それは持っている すべての数値を表現するzend_longフィールドを。このフィールドのタイプ lval



は、プラットフォームによって異なります。64ビットプラットフォームでは、 フィールドは64ビット番号として表され、 32ビットプラットフォームで は、32ビット番号として表されます。



# 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

      
      





つまり、0xff、0xffff、0xffffff、またはその他のものを保存する必要があるかどうかは関係ありません。PHPでは、これらの値はすべて、32ビットまたは64ビットの長さのlong(lvalとして保存され ます。



たとえば、私は最近、マイクロプロセッサをエミュレートする実験をしました。また、メモリの内容と操作を正しく処理する必要がありましたが、ホスティングマシンが数桁のコストを補っていたため、メモリ効率はそれほど必要ありませんでした。



もちろん、C拡張機能やFFIについて話すとすべてが変わりますが、これも私の目標ではありません。私は純粋なPHPについて話している。



したがって、覚えておいてください。それは機能し、希望どおりに動作できますが、ほとんどの場合、タイプはメモリを非効率的に浪費します。



バイナリ表現と16進表現の簡単な紹介



PHPがバイナリデータを処理する方法について説明する前に、まずバイナリとは何かについて説明する必要があります。これについてすでにすべて知っていると思われる場合は、PHPのバイナリ番号と文字列」の章に進んでください



数学には「基礎」という概念があります。さまざまな形式で数量を表す方法を定義します。人々は通常10進法(底10)を使用します。これにより、0、1、2、3、4、5、6、7、8、および9の数字で任意の数を表すことができます。



次の例を明確にするために、数20を「10月20日」。



バイナリ番号(ベース2)は任意の番号を表すことができますが、0と1の2桁のみを使用します。



このようなバイナリルックスで小数点以下20:0b000 10100。自分で使い慣れた形式に変換する必要はありません。コンピュータに変換させてください。 ;)



16進数(基数16)は、10桁の0、1、2、3、4、5、6、7、8、9、およびラテンアルファベットからの追加の6文字を使用して任意の番号を表すことができます:a、b、c 、d、eおよびf。



16進形式の10進20は、0x14のようになります。コンピュータに変換を任せてください、彼らはこれの専門家です!



数値は、バイナリ(ベース2)、オクタル(ベース8)、デシマル(ベース10、通常)、およびヘキサデシマル(ベース16)の異なるベースで表すことができることを理解することが重要です。



PHPや他の多くの言語では、2進数他の言語と 同じように記述されますが、 接頭辞0b:10進数の20は0b 00010100のようになります 。16 数の 接頭辞は0x:10進数の20は 0x 14の



ようになりますご存知かもしれませんが、コンピューターはリテラルデータを保存しません。 ..。それらはすべて、2進数、0、1の形式で表されます。記号、数字、文字、指示-すべてがベース2に表示されます。文字は数字シーケンスの単なる慣例です。たとえば、文字「a」はASCIIテーブルの番号97です。



ただし、すべてがバイナリで保存されている間、プログラマは16進形式でデータを読み取るのが最も快適です。彼らはそのようによく見えます。見てください:



# string "abc"
'abc'

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

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

      
      





バイナリ形式は視覚的に多くのスペースを占有しますが、16進データはバイナリ表現と非常によく似ています。したがって、通常、低レベルのプログラミングで使用します。



転送操作



キャリーの概念についてはすでにご存知ですが、さまざまな理由で使用できるように注意する必要があります。



10進数のセットには、0から9までの数字を表す10個の個別の数字があります。しかし、9より大きい数字を表現しようとすると、数字が失われます。そして、ここで転送操作が適用されます。番号の前に数字1を付け、右の数字を0にリセットします。



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

      
      





バイナリベースは同じように動作しますが、0と1の数字に制限されているだけです。



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

      
      





16進数のベースでも同じですが、範囲がはるかに広いだけです。



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

      
      





ご存知のように、キャリー操作では、特定の数値を表すためにより多くの桁が必要です。これにより、特定の種類のデータがどの程度制限されているか、およびそれらがコンピューターに格納されているため、それらのバイナリ表現がどの程度制限されているかを理解できます。



コンピュータメモリ内のデータ表現



上で述べたように、コンピューターはすべてをバイナリ形式で保存します。つまり、メモリには0と1のみが含まれます。



この概念を、1行と多数の列(メモリ容量が許す限り)の大きなテーブルとして視覚化するのが最も簡単です。各列は2進数(ビット)です



。8ビットを使用したこのようなテーブルでの10進数の20表現は次のようになります。



位置(住所) 0 1 2 3 4 5 6 7
ビット 0 0 0 1 0 1 0 0


符号なし8ビット整数は、最大8つのバイナリ番号を使用して表すことができる数値です。つまり、 0b11111111(10進数の255)が最大の符号なし8ビット数になります。これに1を追加すると、キャリー操作を使用する必要があります。これは、同じ桁数で表すことができなくなります。



これを知っていると、メモリ内に数値の表現が非常に多い理由とその内容を簡単に理解できます。uint8は符号なし8ビット整数(10進数の0〜255)、uint16は符号なし16ビット整数(10進数0〜65535)です。 )。 uint32、uint64、そして理論的にはより高いものもあります。



負の値を表すことができる符号付き整数は、通常、最後のビットを使用して、それらが正(最後のビット= 0)であるか負(最後のビット= 1)であるかを判別します。ご想像のとおり、同じ量のメモリに小さな値を保存できます。符号付き8ビット整数の範囲は-128から10進数127です。これ



が10進数-20で、符号付き8ビット整数として表されます。最初のビットが設定されていることに注意してください(アドレス0、値1)。これは負の数を意味します。



位置(住所) 0 1 2 3 4 5 6 7
ビット 1 0 0 1 0 1 0 0


これまでのところすべてが明確であることを願っています。この紹介は、コンピューターの内部動作を理解するために不可欠です。これを覚えておいてください。そうすれば、PHPが内部でどのように機能するかを常に理解できます。



算術オーバーフロー



選択した数値表現(8ビット、16ビット)によって、範囲の最小値と最大値が決まります。数字がメモリにどのように格納されるかがすべてです。2桁の数字1に1を追加すると、キャリー操作が発生します。つまり、現在の数字のプレフィックスとして別のビットが必要になります。整数形式は非常に注意深く定義されているため、範囲外のキャリー操作に依存することはできません(実際には可能ですが、かなりクレイジーです)。



位置(住所) 0 1 2 3 4 5 6 7
ビット 1 1 1 1 1 1 1 0


ここでは、8ビットの制限(10進数で255)に非常に近づいています。1つ追加すると、バイナリで10進数の255が得られます。



位置(住所) 0 1 2 3 4 5 6 7
ビット 1 1 1 1 1 1 1 1


すべてのビットが割り当てられます!1を追加するには、ビットが不足しているため不可能なキャリー操作が必要になります。8つすべてがすでに割り当てられています。この状況はオーバーフローと呼ばれ 、特定の制限を超えています。バイナリ操作255+ 2は、8ビットの結果1を与えるはずです。



位置(住所) 0 1 2 3 4 5 6 7
ビット 0 0 0 0 0 0 0 1


この動作は偶発的なものではなく、新しい値は特定のルールを使用して計算されますが、ここでは考慮しません。



PHPのバイナリ番号と文字列



PHPに戻りましょう!この大きな逸脱については申し訳ありませんが、それは重要だと思います。



すでに頭の中にパズルのピースがあることを願っています:バイナリ番号、それらの格納方法、オーバーフロー、PHPは数値をどのように表すか...



PHPで整数値として表される10進数の 20は、プラットフォームに応じて2つの異なる表現を持つことができます..。 x86プラットフォームでは、32ビット表現になり、x64では64ビットになりますが、どちらの場合も符号があります(つまり、値は負になる可能性があります)。 10進数の20は8ビットのスペースに収まることがわかっていますが、PHPは任意の10進数を32ビットまたは64ビットとして扱います。



PHPには、関数を使用して前後に変換できるバイナリ文字列もあります パック()および アンパック()



PHPでは、バイナリ文字列と数値の主な違いは、文字列にはバッファのようにデータが含まれているだけであるということです。整数値(バイナリおよびだけでなく)を使用すると、それ自体で算術演算を実行できるだけでなく、AND、OR、XOR、NOTなどのバイナリ(ビット単位)値も実行できます。



バイナリ:PHPで使用する数字または文字列は何ですか?



通常、データの転送にはバイナリ文字列を使用します。したがって、バイナリファイルまたはネットワーキングを読み取るには、バイナリ文字列をパックおよびアンパックする必要があります。



ただし、ORやXORなどの実際の操作は文字列では確実に実行できないため、数字を使用する必要があります。



PHPでのバイナリ値のデバッグ



それでは、楽しんでPHPコードを試してみましょう。



まず、データを視覚化する方法を紹介します。私たちは自分たちが何を扱っているのかを理解しなければなりません。



整数のデバッグは非常に簡単です。sprintf()関数を使用できます 。非常に強力なフォーマットがあり、使用している値をすばやく理解するのに役立ちます。



10進数の20を8ビットのバイナリと1バイトの16進数で表します。



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

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

// Output:
00010100
14

      
      





このフォーマットは 、8桁()のバイナリ表現( )で %08b



変数を出力します この形式で は、変数2桁の16進数表記( )で表示されます )。 $n



b



08







%02X



$n



X



02







バイナリ文字列の視覚化



PHPでは、整数は常に32ビットまたは64ビットの長さですが、文字列の長さはその内容の長さと同じです。それらのバイナリ値をデコードしてレンダリングするには、各バイトを調べて変換する必要があります。



幸い、PHPでは、文字列は配列のように名前が付けられておらず、各位置は1バイトの文字を指しています。シンボルにアクセスする例を次に示します。



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

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

// Outputs:
php

      
      





1文字が1バイトであるとすると、ord()呼び出して1バイトの整数にキャストできます



<?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

      
      





これで、hexdumpコマンドラインアプリケーションで再確認できます。



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

      
      





最初の列にはアドレスのみが含まれ、2番目の列には文字p



h



表す16進値が表示されます p







また、バイナリ文字列を処理する場合は、pack ()関数unpack()関数 使用できます。これは、すばらしい例です。一部のデータ(EXIFなど)を抽出するためにJPEGファイルを読み取る必要があるとします。バイナリ読み取りモードを使用すると、ファイルハンドラーを開いて、最初の2バイトをすぐに読み取ることができます。



<?php

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

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

      
      





値を整数配列に抽出するには、単純にそれらを解凍できます:



$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

      
      





関数のC形式は、unpack()



文字$soi



を符号なし8ビットの数値として文字列に変換する ことに注意してください モディファイア *



は行全体を解凍します。



ビット単位の操作



PHPは、必要になる可能性のあるすべてのビット単位の操作を実装します。それらは式として組み込まれており、それらの作業の結果を以下に説明します。



Phpコード 名前 説明
$ x | $ y 包括的または $ xと$ yには、すべてのビットが指定された値が割り当てられます。
$ x ^ $ y 排他的または $ xまたは$ yには、指定されたビットの値が割り当てられます。
$ x&$ y そして $ xと$ yには、指定されたビットの値が同時に割り当てられます。
〜$ x ない $ xのすべてのビットの値を変更します。
$ x << $ y 左方移動 $ xのビットを$ yの位置だけ左にシフトします。
$ x >> $ y 右シフト $ xのビットを$ yの位置だけ右にシフトします。


それぞれがどのように機能するかを説明します!



ましょう $x = 0x20



$y = 0x30



以下に、バイナリ表記を使用した例を示します。



包括的または($ x | $ y)のしくみ



包括的OR演算は、両方の入力からすべてのビットを取得します。つまり、 $x | $y



を返す必要があり 0x30



ます。見てください:



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

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

      
      





注:右から左に、6番目のビット$x



(1)と、5番目と6番目のビットが指定されいます $y



データはプールされ、5番目と6番目のビットが与えられた値が生成されました 0x30







排他的または($ x ^ $ y)のしくみ



排他的OR操作(XORとも呼ばれます)は、片側からのみビットを取得します。つまり、計算の結果は次のように $x ^ $y



なります 0x10







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

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

      
      





AND($ x&$ y)のしくみ



AND演算子ははるかに理解しやすいです。各ビットにAND演算を適用するため、両側で互いに等しい値のみが取得されます。計算結果は次のように $x & $y



なります 0x20







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

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

      
      





NOT(〜$ x)のしくみ



NOT操作には1つのパラメーターが必要であり、送信されたすべてのビットの値を変更するだけです。すべての0を1に、すべての1を0に変換します。:



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

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

      
      





この操作をPHPで実行しsprintf()



を使用してデバッグすることにした 場合、おそらくより広い数値に気づいたでしょうか。数値の正規化の章で は、ここで何が起こっているのか、そしてそれを修正する方法を説明します。



左SHIFTと右SHIFTのしくみ($ x << $ nと$ x >> $ n)



ビットシフトは、数値を2の累乗で乗算または除算することに似ています。すべてのビットは $n



左または右の位置に移動します。



たとえば、表示しやすくするために小さなバイナリ番号を取り $x = 0b0010



ます。一度$x



に移動すると 、その1ビットは1つの位置を左に移動するはずです。



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

      
      





右にオフセットした同じもの:



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

      
      





つまり $n



、左に $n



シフト$n



することは2倍することと同じであり、右にシフト することは2で割ることと同じ $n



です。



ビットマスクとは



これらの操作やその他の手法を使用すると、多くの興味深いことができます。たとえば、ビットマスクを適用します。これは、非常に具体的な情報を抽出するために作成された、任意のバイナリ番号です。



たとえば、8ビットの符号付き数値は8番目のビット(0)が指定されていない場合は正であり、ビットが指定されている場合は負であるという考えを考えてみましょう。数字は正0x20



ですかですか?どう 0x81



ですか?



これに答えるために、単一の負のビットが指定された(0b10000000



、同等に 0x80



非常に便利なバイトを作成し、 0x20



それANDすることができます。結果が 0x80



0b10000000



、私たちのマスク)、それは負の数です、そうでなければそれは正です:



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

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

      
      





これは、フラグを操作するときによく必要になります。エラーメッセージフラグなど、PHP自体での使用例もあります



生成されるエラーの種類を選択できます。



error_reporting(E_WARNING | E_NOTICE);

      
      





何が起きてる?あなたの意味を見てください:



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

      
      





PHPは、送信可能な通知を検出すると、次のようなものをチェックします。



// 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

      
      





そして、あなたはそれをどこでも見るでしょう!バイナリ、プロセッサ、あらゆる種類の低レベルのもの!



数値の正規化



PHPには、2進数の処理に関連する1つの特徴があります。整数は、サイズが32ビットまたは64ビットです。これは、計算を信頼するために、それらを正規化する必要があることを意味します。



たとえば、64ビットマシンでこの操作を実行すると、奇妙な(しかし予想される)結果が得られます。



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

// Expected
0b11011111
// Actual
0b1111111111111111111111111111111111111111111111111111111111011111

      
      





ここで何が起こったのですか? 8ビット整数(0x20



に対するNOT操作は、 すべてのゼロビットを1に変換しました。ゼロがあったと思いますか?そうです、以前は無視されていた左側の他のすべての56ビット!



繰り返しますが、その理由は、PHPでは、値に関係なく、整数の長さが32ビットまたは64ビットであるためです。



ただし、コードは期待どおりに機能します。たとえば、〜操作の結果は 0x20 & 0b11011111 === 0b11011111



ブール値(true)になります。ただし、左側のこれらのビットはどこにも移動しないことを忘れないでください。そうしないと、奇妙なコード動作が発生します。



この問題を解決するには、すべてゼロをクリアするビットマスクを適用して数値を正規化できます。たとえば、正規化するには ~0x20



前の56ビットがすべてゼロになるように、8ビット整数は0xFF



0b11111111



とAND演算する必要があります



~0x20 & 0xFF
-> 0b11011111

      
      





注意!変数の内容を忘れないでください。そうしないと、予期しない動作が発生します。たとえば、上記の値を8ビットマスクなしで右にシフトするとどうなるかを見てみましょう。



~0x20 & 0xFF
-> 0b11011111

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

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

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

      
      





説明させてください。PHPの観点からは、64ビットの数値を明示的に処理しているため、これは予想されます。あなたのプログラムが何を待っているのかを理解する必要があります。



ヒント:TDDパラダイムでプログラミングすることにより、これらのばかげた間違いを避けてください



結論:バイナリとPHPはかっこいい



このようなツールを使用すると、他のすべては、バイナリまたはプロトコルの動作に関する正しいドキュメントを見つけるだけになります。結局のところ、すべてがバイナリシーケンスです。



PDFまたはEXIFの仕様を読むことを強くお勧めします。MessagePackシリアル化フォーマット、またはAvro、Protobufの独自の実装を試してみることもできます ...可能性は無限大です!



All Articles