在庫管理

在庫管理

学習演習

はじめに

整数

2進表記

整数とは、4、-2、0、64532のような数を表す抽象的な概念です。

整数をバイトの並びとして表すには、2進表記を使います。 この表記では、並びの中の各ビットがそれぞれ異なる2の累乗を表し、ビットのインデックスが右から左へ増えるにつれて、その値は大きくなります。

符号なし数値

その数値が負にならない場合、それを符号なしの数値と呼びます。

符号なしの数値は、その並びの中で1になっているすべてのビットに対応する2の累乗の和として、そのまま表されます。

レジスタで表せる負でない整数の範囲は、0(どのビットも1になっていない状態)から2⁶⁴ - 1(64個のビットがすべて1になっている状態の和)までです。

符号なしの数値をより大きなサイズに広げるには、上位のビットをすべて0で埋めます。こうすると、新しいビットが値に寄与することはありません。 これをゼロ拡張と呼びます。

movzx命令(zはゼロの意味)は、8ビットまたは16ビットのソースオペランドを、より大きな宛先オペランドへゼロ拡張します。 32ビットのソースオペランドは、単純なmovによって、常に宛先オペランドの64ビットすべてへゼロ拡張されます。

符号あり数値

整数が正の値と負の値の両方を取りうる場合、それを符号ありの数値と呼びます。

負の数を表すために、x86-64では2の補数表現を使います。

2の補数では、符号ありの数値も、1になっているビットに対応する2の累乗の和として表されます。 ただし、最上位のビットが1になっている場合は、そのビットは他のビットに加算されるのではなく、減算されます。

このビットは他のすべてのビットの和よりも大きな値に対応するため、実際にはこのビットが1の数値は常に負になります。 この特別なビットを符号ビットと呼びます。

符号ありの数値をより大きなサイズに広げるには、新たに増えた上位ビットをすべて符号ビットのコピーで埋めます。こうすると値が保たれます。 これを符号拡張と呼びます。

movsx命令(sは符号の意味)は、8ビットまたは16ビットのソースオペランドを、より大きな宛先オペランドへ符号拡張します。 movsxの派生であるmovsxdは、32ビットのソースオペランドから64ビットの宛先オペランドへ、同じことを行います。

neg命令を使うと、数値の符号を変えられます。

Caution

アセンブリでは、あるバイトの並びが符号ありの数値を表しているのか、符号なしの数値を表しているのかを見分ける方法はありません。 そのバイトに意味を与えるのは、プログラマーの責任です。

コメントを使うと、この作業がぐっと楽になります。

即値

前の概念で、4や-15のような定数を、多くの命令のソースオペランドとして使えることに触れました。 こうした数値を即値と呼びます。

即値はレジスタやメモリに保持されるのではなく、命令そのものの中にエンコードされます。 ほとんどの命令では、宛先オペランドがどれほど大きくても、即値のために確保される領域はわずか32ビット幅です。

宛先オペランドが64ビット幅の場合、その32ビットは、それを埋めるために_符号拡張_されます。 オペランドの上半分は、即値の最上位ビットのコピーで完全に埋められます。そのため、この方法で書けるのは、_32ビット符号あり整数_の範囲の数値だけです:

add rax, -1          ; the immediate is sign-extended, so all 64 bits of rax are affected
add rax, 2147483647  ; the largest immediate an instruction like this accepts

その範囲外の数値は、即値として使えません。 この規則の例外はmovで、宛先オペランドがレジスタの場合は64ビットの即値をそのまま受け取れます。 64ビットの即値が必要な場合は、まずmovでそれをレジスタに読み込み、そのレジスタを使ってください:

mov rax, 3435973837           ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax

負の即値と、同じビット表現を持つ符号なしの数値は等価であり、まったく同じ値にアセンブルされることに注意してください:

mov rax, -1                   ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1

加算

2つの数値の加算は、add命令で計算できます。

1オペランド命令のincもあり、これはオペランドの値に1を加えます:

inc rax ; rax = rax + 1

2つの整数の和は、符号なしの数値でも符号ありの数値でも、同じように計算されます。

減算

2つの整数の減算は、sub命令で行います。

1オペランド命令のdecもあり、これはオペランドの値から1を引きます:

dec rax ; rax = rax - 1

2つの整数の差も、符号なしの数値でも符号ありの数値でも、同じように計算されます。

乗算

x86-64で2つの数値の乗算を行うには、2つの異なる命令があります。 原則として、符号なしの乗算にはmul命令を、符号ありの乗算にはimulを使います。

mul命令は次の1オペランド形式をとります。ここでsrcはソースオペランドです:

mul src

imul命令は、1オペランド、2オペランド、3オペランドの形式をとれます:

imul src
imul dest, src
imul dest, src1, src2
1オペランドの乗算

1オペランド形式の乗算では、2つのレジスタが暗黙的に使われます。raxとrdxです。 2つの64ビット数の乗算であれば、結果の下位64ビットがraxに、上位64ビットがrdxに入ります。

これは通常rdx:raxと呼ばれ、2つのレジスタが連携して使われることを示します:

mul rcx ; rax = lower 64 bits of rax * rcx
        ; rdx = upper 64 bits of rax * rcx

他のオペランドサイズでも同じことが起こります。 たとえば、2つの32ビット数を掛ける場合は、eaxとedxが使われます。

例外は、2つのバイト同士の乗算です。

この場合、dl:alではなくaxが使われます。 axの下位部分(al)が積の下位8ビットを、上位部分(ah)が上位8ビットを受け取ります。

Caution

乗算で暗黙的に使われるレジスタ(raxやrdxなど)は、必ず上書きされます。 それらのレジスタの値が後で必要なら、演算の前に保存しておいてください。

2オペランドの乗算

imulの2オペランド形式には、明示的な宛先オペランドがあり、通常の構文に従います。 rdxは使われません。 代わりに、結果は宛先オペランドに収まるように切り捨てられます。

imul r8, r9 ; r8 = lower 64 bits of r8 * r9
3オペランドの乗算

imulの3オペランド形式には2つのソースオペランドがあり、その2つ目は常に即値(定数)です。 2つのソースオペランドが掛け合わされ、結果は切り捨てられて宛先オペランドに格納されます:

imul r8, r9, 100 ; r8 = lower 64 bits of r9 * 100

宛先オペランドは乗算には使われないことに注意してください。 単に結果を受け取るだけです。

オーバーフローの扱い

2オペランド形式と3オペランド形式の乗算はどちらも、結果を宛先オペランドのサイズに収まるように切り捨てます。 1オペランド形式の乗算は範囲全体を保ちますが、通常はrdxとraxの2つのレジスタに分けて格納されます。

そのため、積全体を1つのレジスタに収めるために、乗算の前にオペランドを広げておくと便利なことがあります。 符号なしのオペランドはゼロ拡張し、符号ありのオペランドは符号拡張します:

movzx eax, di ; di and si hold unsigned 16-bit numbers
movzx ecx, si
mul ecx       ; the 32-bit product fits in eax, and edx is cleared

除算

乗算の場合と同じく、2つの数値の除算を行う命令も2つあります。 符号なしの除算にはdiv命令を、符号ありの除算にはidivを使います。

どちらの命令も、1つのオペランドだけで動作します:

div src
idiv src

16ビット、32ビット、64ビットの除算では、それぞれdx:ax、edx:eax、rdx:raxを被除数として使います。 この場合、2つのレジスタが連携して2Nビットの値を作ります。ここでNは演算のサイズ(16ビット、32ビット、64ビット)です。 この値がソースオペランドで割られます。 演算のサイズに応じて、商はax、eax、raxに、余りはdx、edx、rdxに書き込まれます。

バイト同士の除算は特別で、dl:alではなくaxを使います。 axの下位8ビット(al)が演算の商を、上位8ビット(ah)が余りを受け取ります。

除算の前に、被除数のすべてのビットを適切に設定しておく必要があることに注意してください。 rdx(8ビット除算ではah)に1になっているビットがあれば、それは割られる値に寄与します。

符号なしの除算では、割られる値が下半分に収まる場合、上半分をクリアしておく必要があります。 それらのビットをクリアする命令なら、どれでもかまいません。 たとえば、mov edx, 0は32ビット除算で上位ビットをクリアします。

符号ありの除算では、代わりにその値を符号拡張します。 この処理を自動で行う命令があります。cbw、cwd、cdq、cqoです。 最初のcbwは、alの符号に応じてahのビットを設定します。 残りの命令は、それぞれaxからdxへ、eaxからedxへ、raxからrdxへ符号拡張を行います。

Caution

除算で暗黙的に使われるレジスタ(raxやrdxなど)は、必ず上書きされます。 それらのレジスタの値が後で必要なら、除算の前に保存しておいてください。

説明

地元の店が、在庫をより大きな倉庫へ移すことになりました。 すべてを荷造りして運び出すために、あなたは雇われました。

タスクは4つあり、どれも輸送の管理に関するものです。

Note

このコンセプトで扱う命令は次のとおりです。

命令 説明
add a, b a = a + b
inc a a = a + 1
sub a, b a = a - b
dec a a = a - 1
imul a rdx:rax = a * rax(符号付き)
imul a, b a = a * b(符号付き、切り捨て)
imul a, b, c a = b * c(符号付き、切り捨て)
mul a rdx:rax = a * rax(符号なし)
div a rax = 商、rdx = rdx:rax / a の余り(符号なし)
idiv a rax = 商、rdx = rdx:rax / a の余り(符号付き)
movzx a, b a = b、余分なビットに0を加える
movsx a, b a = b、b < 0 なら余分なビットに1を、そうでなければ0を加える
Note

オペランドの名前を変えると、同じレジスタに異なるサイズでアクセスできることを覚えておいてください。 たとえば、rax(64ビット)、eax(32ビット)、ax(16ビット)、al(8ビット)です。

完全な表は、前のコンセプトを参照してください。

1. 各箱の重さを求める

品物は、重さを記入したラベルを貼る必要がある箱に詰められていきます。 近くに秤はありませんが、幸いなことに、それぞれの品物が平均でどれくらいの重さなのかは分かっています。

整理しやすいように、1つの箱には2種類の製品の品物だけを入れます。

箱の合計の重さをg単位で返す関数get_box_weightを定義します。 この関数は、次の順番で引数を取ります。

  • 箱に入っている1つ目の製品の品物の数
  • 1つ目の製品の品物1つの重さ(g単位)
  • 箱に入っている2つ目の製品の品物の数
  • 2つ目の製品の品物1つの重さ(g単位)

空の箱の重さは500 gとします。 定数WEIGHT_OF_EMPTY_BOXは解答ファイルの先頭で定義されています。

例:

get_box_weight(30, 40, 50, 20);
// => 2700

すべての引数は16ビットの非負整数で、戻り値は32ビットの非負整数です。

2. トラックに積める箱の数を計算する

箱は積み重ねられ、トラックで新しい倉庫へ運ばれます。 ただし、トラックの内側の高さには限りがあります。

ある高さの箱をトラックの中に垂直に(上に重ねて)何箱積めるかを返す関数max_number_of_boxesを定義します。

この関数は、箱の高さ(cm単位)を引数に取ります。 トラックの内側の高さは300 cmとします。 定数TRUCK_HEIGHTは解答ファイルの先頭で定義されています。

例:

max_number_of_boxes(30);
// => 10

引数と戻り値は8ビットの非負整数です。 箱の高さは常に2以上なので、結果は8ビットに収まります。

3. すべての製品が確認済みかチェックする

新しい倉庫には、製品ごとにまだ確認できていない品物の数が書かれたチェックリストがあります。 そこに新しい箱を運び込むたびに、その箱に入っている各製品について、チェックリストの新しい値を計算する必要があります。

ある製品について、新しい倉庫へあと何個の品物を運ぶ必要があるかを返す関数items_to_be_movedを定義します。 この関数は、次の順番で引数を取ります。

  • 製品についてまだ確認できていない品物の数
  • 箱に入っているその製品の品物の数

例:

items_to_be_moved(76532, 120);
// => 76412

引数は32ビットの非負整数です。 戻り値は32ビットの整数です。 処理の途中でエラーが起きた場合、結果が負の数になることもあります。

4. 報酬を受け取る

報酬は、運んだ箱の数と、必要だったトラックの運行回数によって決まります。 箱1つにつき5ドル、運行1回につき220ドルが支払われます。 定数PAY_PER_BOXとPAY_PER_TRUCK_TRIPは解答ファイルの先頭で定義されています。

初期費用をまかなうために、この報酬の一部を前払いで受け取っている場合があり、その前払い分は最終的な報酬から差し引く必要があることに注意してください。 さらに、保険が適用されない製品もあり、そうした品物が壊れたり見つからなかったりした場合は、その分の価値も報酬から差し引かれます。 気をつけないと、逆にお金を支払うことになるかもしれません。

つまり、受け取る(または支払う)正味の金額は次のとおりです。

net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value

この報酬、または借金は、雇った作業員と均等に分け合います。 割り切れずに残った金額や借金は、そのまま自分の取り分になります。 たとえば、正味の金額が100で、それを6人(自分と5人の作業員)で分ける場合、受け取るのは20です(100/(5 + 1) = 16に、残りの4を足したもの)。

最終的に受け取る、または支払う金額を返す関数calculate_paymentを定義します。 この関数は、次の順番で引数を取ります。

  • 前払いで受け取った金額(64ビットの非負整数)
  • 運んだ箱の合計数(32ビットの非負整数)
  • 行ったトラックの運行回数(32ビットの非負整数)
  • 壊れた品物、または見つからなかった品物の数(32ビットの非負整数)
  • 失われた品物1つあたりの価値(64ビットの非負整数)
  • 報酬や借金を分け合う作業員の数(8ビットの正の整数)

例:

calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029

戻り値は64ビットの整数です。

GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
x86-64 Assembly Exercism

在庫管理を始める準備はできましたか?

Exercismに登録すれば、22個のコンセプト130個の演習、そして本物の人間によるメンタリングとともに、x86-64 Assemblyを学んでマスターできます。すべて無料です。