Bashのビット単位の操作は期待どおりに機能しません。

Bashのビット単位の操作は期待どおりに機能しません。

奇妙な問題が発生しました。説明のために、私のコンピュータから最大の符号なし番号(printf "%X \n" -1私に提供されているFFFFFFFFFFFFFFFF)を取得していくつかのビットを移動しましょう。まず左に移動します。

printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF<<4 ))
FFFFFFFFFFFFFFF0
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF<<8 ))
FFFFFFFFFFFFFF00
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF<<16 ))
FFFFFFFFFFFF0000

今まではそんなに良くなった。予想通り。それでは右に移動してみましょう。

printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF>>4 ))
FFFFFFFFFFFFFFFF
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF>>8 ))
FFFFFFFFFFFFFFFF
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF>>16 ))
FFFFFFFFFFFFFFFF

何を待つ? ?なぜこれがうまくいかないのですか?これはバグですか?


編集する:

誰かが提案された符号ビットへの接続を提案するかどうか心配です。しかし、私たちは算術について話しているわけではないので、ここでは記号の概念に立つ場所はありません。*などの他のツールは/算術に使用されます。ビットを操作できるツールのポイントは、ビットを操作できることです。後でそのビットを署名するかどうかにかかわらず表示するように選択する方法に関係なく。正しいですか?良い:

printf "%u \n" -1
18446744073709551615

誰でもどんなアイデアがありますか?

編集する:

ここの答えは乗算や除算について直接議論するので、私の懸念をより明確に説明します。乗算/除算とビットシフトは2つの異なるものですが、長い間プログラマの心の中でこれらの間の関連性を見ることができました。算術を実行するときは、ビットシフトには符号の概念が必要です。 Bashには、これら2つの異なるタスクを処理するための2つのツールセットがあります。数字に2を掛けたい場合は、この*ツールを使用してください。 Bashが内部的にビットシフトを使用して算術を実行できるという事実は要点を超えています。

答えの1つを引用すると...

符号ビットがコピーされない場合、結果は符号なしである。たとえば、1111 00008ビット値を右に1回移動すると、0111 1000

しかし、それは私が望んでいたまさに1111 0000それであることがわかりました。0111 1000除算をしたい場合は算術演算子を使用します。

とにかく、移動時にどのビットを埋めるべきかを明示的に指定する方法はありますか?

答え1

持つ右に移動する2つの異なる方法一般的に使用されます。

「論理右シフト」は左側に0ビットを挿入するため、1つの位置だけ右にシフトした結果は、符号なしの2進数を2で割った値に対応します。echo $(( 16 >> 1 ))与えられた8

また、算術右シフトは符号ビットのコピーを左に挿入するため、1ビットを右に移動した結果は次のように分割することになります。兆候2進数を2で割ります。 、およびを提供しますecho $(( 16 >> 1 ))。 2の補数を除いて、実際の除算の丸めと一致しません。8echo $(( -16 >> 1 ))-8-15 >> 1-8-15 / 2-7

符号ビットがコピーされずにクリアされると、結果は正数になります。たとえば、1111 00008ビット値(0xf0、-16)を右に1回移動すると0111 1000(0x78、+120)となります。


今、これらのどれを使用するかは、より厳しい質問です。

実際、多くの実装では符号付き数値に算術シフトを使用しますが、シェル算術はほとんど符号付き long に対して実行されます。

しかし、これが完全な保証ではありません。シェル操作のPOSIX定義は、ほとんどの動作のC標準を参照します。たとえば、演算子テーブルには>>どのような種類の移動を実行すべきかは明記されていません。 (望むより:シェルコマンド言語、2.6.4 算術拡張そしてShell and Utilities、1.1.2 ISO C規格から派生した概念:算術精度と演算)

オペランドとオプション引数の値を含む整数変数と定数[...]は、ISO C標準符号付き長いデータ型と同じように実装されています[...]

算術演算子および制御フローキーワードの実装は、参照されているISO C標準部分[...]の実装と同じでなければなりません
<<>>セクション6.5.7、ビット単位のシフト演算子

cppreference.comでC演算子について話します。それ

negative の場合、a値はa >> b実装によって定義されます(ほとんどの実装では算術右シフトを実行するため、結果は負のままです)。

(これはおそらくすべてが2の報酬ではなかった世界の残骸であろう。 . 実装が定義されている)。

他のプログラミング言語、JavaScriptのように、別の算術右シフト>>と論理右シフト演算子を使用します>>>。しかし、Cはそうではなく、私が試したシェルも同じです。

また、単語の幅より大きいオフセットを使用してシフトを実行すると、奇妙なことが発生することがわかります。 x86 では、プロセッサが移動された値の最低 6 ビットしか表示されない1 << 64ため。ただし、他のプロセッサでは結果が異なる場合があります。11 << 0(1 << 32) << 320


あなたは言う、

しかし、ここでシンボルの概念は立場がありません。私の言葉は、後で署名されているか署名されていない状態で表示することを選択しても、数字は数字であるということです。そうですか?

2の補数機械(例:32x32 - > 32)で加算、減算、乗算のサブ部分についても同様です。

ただし、これは一般的な乗算や除算の高次部分には該当しません。 8 ビット値は、0xff符号なしの数値 255 または符号付きの数値 -1 を表すことができます。たとえば、8x8 - > 16乗算は、符号付き値(-1 * -1)か符号なし値(255 * 255)かによって、OR0xff * 0xffです。また、たとえば、符号があるか(-1/3 == 0)、または符号がないか(255/3 == 85)に応じて、isまたはです。0x00010xfe010xff / 300x55

答え2

  • 論理的な右シフトはゼロで埋められます。

    [01010110] >> 2 becomes [00010101]
    [11010110] >> 2 becomes [00110101]
    
  • 算術右シフトは最も重要なビットを埋めます。

    [01010110] >> 2 becomes [00010101]
    [11010110] >> 2 becomes [11110101]
    

論理シフトを期待していましたが、Bashは算術シフトを実行します。それは実際に署名についてです。

これ手動説明する

評価は固定幅整数で行われます。演算子とその優先順位、結合性、値はC言語と同じです。

整数定数はC言語定義に従い、サフィックスや文字定数はありません。 0で始まる定数は8進数として解釈されます。前に「0x」または「0X」がある場合は、16進数を表します。

そして引用したものコンピュータシステム、Randall Bryant、David O'Haralan:

C標準は、どのタイプの右シフトを使用すべきかを正確に定義しません。符号なしデータの場合、右シフトは論理的でなければなりません。符号付きデータ(デフォルト)の場合、算術または論理シフトを使用できます。 (...)しかし、実際には、ほぼすべてのコンパイラ/マシンの組み合わせが署名されたデータに対して算術右シフトを使用し、多くのプログラマがこれが真であると仮定します。

誰かがStack Overflowで次の質問をしました。Cのシフト演算子(<<、>>)は算術ですか、それとも論理ですか?

答え3

Bashは他のシェルのように動作します。シェルはかなり高いレベルの言語ですが、一般的な整数演算の代わりに限られたサイズの演算を提供することは設計エラーである可能性がありますが、現時点では変わりません。

後で符号付きまたはなしとしてマークすることを選択したかどうかに関係なく、数字は数字です。そうですか?

はい、数字は数字です。しかし、シェルはそうではありません。実数整数。彼らだけ機械整数、彼らはより制限的で奇妙な方法で動作します。

乗算/除算とビットシフトは2つの異なるものですが、長い間プログラマの心の中でこれらの間の関連性を見ることができました。算術を実行するときは、ビットシフトには符号の概念が必要です。

実は互いに違うが関連しているという事実を誤解しているのです。ビットシフトには確かに符号という概念があります!

機械定数はさまざまな方法で解釈できます。

  • N値ビットの配列として。これが記憶の中の表現だ。
  • ビット配列の場合、最初のビットは符号ビットであり、他のN-1は値ビットである。符号ビットはN値ビット解釈の最上位ビットである。
  • 0と2^N-1の間の整数(「符号なし整数」)として、その値はN値ビットで表されます。
  • -2^(N-1) と 2^(N-1)-1("符号付き整数") の間の整数であり、その値は N-1 値ビットと符号ビットで表されます。符号ビットが0の場合、値は値ビットとして与えられる。符号ビットが1の場合、値は負で、理論的には値ビットに基づいて値を計算する方法がありますが、実際にはUnixシェルを実行して分割するプラットフォームがわかりません。2の報酬。 2の補数表現を使用する場合、符号ビットは1であり、値ビットは整数値を表す。Xはい - (2^(N-1) -X)。
  • 整数モジュロ2^N(「整数モジュロ」)。値は2^Nモジュールで符号なしの値です。 2の補数表現では、これはモジュロ2^Nの符号付き値でもあります(これは2の補数の主な利点です)。

関連するすべての数字が0と2^N-1の間にある限り、任意の解釈を選択でき、すべての操作は直感的な結果を提供します。ただし、オペランドまたは結果がこの範囲外の場合(負数または大きすぎる場合)、どの解釈を使用するかが重要です。

例を読みやすくするために、N = 4(2 ^ N = 16)と2の補数を使用する例を提供します。実際、最新バージョンのbashではN = 64です。以前のバージョンのbashやその他のシェルでは、Nは32ビットプラットフォームで32にすることができます。

  • 数値分析はモジュロ2^Nで行われます。たとえば、N = 4の場合、sum319sumは両方とも-13ビットで表される同じ数字を表し0011-3合計は13ビットで表されます1101
  • 8進数と16進数の印刷では、数字を符号なし整数として扱います。たとえば、ビット表現を持つ数字は110116進形式で印刷されますd。最初のビットは符号ビットではなく値ビットとして解釈されます。
  • 10進印刷では、数値を符号付き整数として扱います。たとえば、ビット表現を持つ数字は110116進形式で印刷されます-3。最初のビットは値ビットではなく符号ビットとして解釈されます。
  • 加算、減算、乗算はモジュロ2^Nで行われます。これは、範囲制限なしで符号なし整数または符号付き整数に対して操作を実行し、次に目的の範囲内で残りのモジュールに2^Nを取るのと同じです。
  • 除算は符号付き整数に対して行われます。加算、減算、乗算とは異なり、この表現の選択は重要です。符号なし整数または数値モジュロを使用すると、他の結果が得られます。 (ねじれがあります:(-2^(N-1)/ -1)は、結果が範囲外の唯一の場合です。私が知る限り、すべてのシェルは値を-2^(N-1)として指定します。) )
  • ビット操作はビット表現に対して機能します。ほとんどの操作では、符号付き表現を選択しても符号なし表現を選択しても構いません。すべてのビットに対して同じ操作が実行されます。ただし、交代勤務の場合には重要であり、次のとおりです。
    • 左シフトは最初のビットを値ビットとして扱います。たとえば、ビット表現が左に1だけ移動した数字には0111ビット表現があります1110。これにより左移動が行われます。K2^を掛けるのと同じです。K
    • 私が見たすべてのシェルの右の動きは、最初のビットを値ビットとして伝播する符号ビットとして扱います。たとえば、ビット表現が右に1だけ移動した数字には1101ビット表現があります1110。これにより、右に移動します。K2^で割るのと同じです。K。これは…算術シフト、。
  • 比較は符号付き整数に対して機能します。たとえば、ビットで表される数字は負の数と見なされるため、ビットで1111表される数字よりも小さいです。00001111

したがって、64ビットの例に戻って0xFFFFFFFFFFFFFFFF>>4yesです0xFFFFFFFFFFFFFFFF。これは、左のオペランドが符号付きビット配列として解釈され、符号設定ビットが上位4つの>>公開値ビット位置に伝播されるためです。とは、同じ機械定数を表す別の方法である-1 >> 4ため、まったく同じように動作します。-10xFFFFFFFFFFFFFFFF

答え4

見る存在する

最初の1(左から)は記号を表しているので、コピーしてください。しようとした場合:

printf "%X \n" $(( 0x7FFFFFFFFFFFFFFF>>4 ))

あなたが期待するものを得ることができます。

関連情報