库存管理

库存管理

学习练习

简介

整数

二进制表示法

整数是一种抽象,用来表示没有小数部分的数,例如4、-2、0或64532。

要把整数表示成字节序列,就要用到二进制表示法。在这种表示法中,序列里的每一位代表 2 的一个不同的幂,位的下标从右向左递增时,这一位所代表的值也随之增大。

无符号数

如果一个数只能取非负值,就称为无符号数。

无符号数直接表示为其序列中所有置位所对应的 2 的幂之和。

寄存器中可表示的非负整数范围,从0(没有位被置位)到2⁶⁴ - 1(64 位全部置位之和)。

把无符号数扩展到更大的位宽时,把所有高位都填上0,这样新增的位不会对值产生任何贡献。这称为零扩展。

指令movzx(z代表 zero)把 8 位或 16 位的源操作数零扩展到更大的目标操作数。32 位的源操作数则总是通过一条简单的mov零扩展到目标操作数的全部 64 位。

有符号数

如果一个数可以取正值或负值,就称为有符号数。

为了表示负数,x86-64 使用补码表示法。

在补码中,有符号数同样表示为置位所对应的 2 的幂之和。不过,如果最高位被置位,它就要被减去,而不是加到其他位上。

由于这一位对应的值比其余所有位之和还要大,实际上这就意味着,只要这一位被置位,这个数就一定是负数。这个特殊的位称为符号位。

把有符号数扩展到更大的位宽,意味着把每个新增的高位都填成符号位的副本,从而保持值不变。这称为符号扩展。

指令movsx(s代表 sign)把 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

加法

两个数相加可以用add指令来计算。

还有一个单操作数指令inc,它把操作数中的值加1:

inc rax ; rax = rax + 1

两个整数相加时,无符号数和有符号数的运算方式相同。

减法

两个整数相减使用sub指令来完成。

还有一个单操作数指令dec,它把操作数中的值减1:

dec rax ; rax = rax - 1

两个整数相减时,无符号数和有符号数的运算方式也相同。

乘法

在 x86-64 中,两个数相乘有两条不同的指令。通常来说,无符号乘法使用mul指令,有符号乘法使用imul。

mul指令采用下面这种单操作数形式,其中src是源操作数:

mul src

imul指令可以接受单操作数、双操作数或三操作数形式:

imul src
imul dest, src
imul dest, src1, src2
单操作数乘法

单操作数形式的乘法会隐式用到两个寄存器:rax和rdx。如果相乘的是两个 64 位数,那么结果的低 64 位会放在rax中,高 64 位会放在rdx中。

这通常写作rdx:rax,表示两个寄存器是配合使用的:

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

其他操作数宽度也是如此。例如,如果相乘的是两个 32 位数,就会用到eax和edx。

例外是两个字节相乘。

这种情况下,用的不是dl:al,而是ax。ax的低半部分(al)会得到乘积的低 8 位,高半部分(ah)会得到高 8 位。

Caution

乘法中隐式使用的寄存器,例如rax和rdx,总是会被覆盖。如果这些寄存器中的值之后还要用到,就应该在运算前保存它们。

双操作数乘法

imul的双操作数形式有显式的目标操作数,遵循通常的语法。它不使用rdx,而是把结果截断,使其能放进目标操作数。

imul r8, r9 ; r8 = lower 64 bits of r8 * r9
三操作数乘法

imul的三操作数形式有两个源操作数,其中第二个始终是立即数(一个常数)。两个源操作数相乘,结果被截断后放入目标操作数:

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

注意,目标操作数并不参与乘法运算,它只是接收结果。

处理溢出

双操作数和三操作数乘法都会截断结果,使其符合目标操作数的大小。单操作数乘法保留完整范围,但结果通常被拆分到两个寄存器中,即rdx和rax。

因此,有时在乘法之前先扩展操作数会很有用,这样可以腾出空间,把整个乘积放进单个寄存器。无符号操作数进行零扩展,有符号操作数则进行符号扩展:

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

除法

和乘法一样,两个数相除也有两条指令。无符号除法使用div指令,有符号除法则使用idiv。

这两条指令都只使用一个操作数:

div src
idiv src

16 位、32 位和 64 位除法分别使用dx:ax、edx:eax和rdx:rax作为被除数。在这些情况下,两个寄存器配合组成一个 2N 位的值,其中 N 是运算的宽度(16 位、32 位或 64 位)。这个值再除以源操作数。根据运算的宽度,商会写入ax、eax或rax,余数则写入dx、edx或rdx。

字节相除比较特殊:用的不是dl:al,而是ax。ax的低 8 位(al)会得到运算的商,高 8 位(ah)会得到余数。

注意,除法之前应当把被除数中的所有位正确设置好。rdx中(8 位除法时是ah中)任何一个被置位的位,都会对将要相除的值产生影响。

无符号除法中,当要相除的值能放进低半部分时,应当把高半部分清零。任何能清掉这些位的指令都可以。例如,在 32 位除法中,mov edx, 0会清除高半部分的位。

有符号除法中,则应当对值进行符号扩展。有一些指令可以自动完成这个过程:cbw、cwd、cdq和cqo。第一条根据al的符号设置ah中的位。其余几条分别执行从ax到dx、从eax到edx以及从rax到rdx的符号扩展。

Caution

除法中隐式使用的寄存器,例如rax和rdx,总是会被覆盖。如果这些寄存器中的值之后还要用到,就应该在除法前保存它们。

说明

一家本地商店要把库存搬到一个更大的仓库。 你受雇负责打包并搬运所有东西。

你有四项任务,都与管理这次搬运有关。

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 (signed)
imul a, b a = a * b (signed, truncated)
imul a, b, c a = b * c (signed, truncated)
mul a rdx:rax = a * rax (unsigned)
div a rax = quotient, rdx = remainder of rdx:rax / a (unsigned)
idiv a rax = quotient, rdx = remainder of rdx:rax / a (signed)
movzx a, b a = b, adding 0 to the extra bits
movsx a, b a = b, adding 1 to the extra bits if b < 0 or 0 otherwise
Note

记住,只要改变操作数的名称,就能用不同的大小访问同一个寄存器。 例如:rax(64 位)、eax(32 位)、ax(16 位)、al(8 位)。

完整表格可以参考上一个概念。

1. 获取每个箱子的重量

物品正在被打包进箱子,每个箱子都必须标上自己的重量。 手边没有秤,不过好在你知道每件物品的平均重量。

为了更好地整理,一个箱子只装两种不同产品的物品。

定义一个函数get_box_weight,返回一个箱子的总重量,单位为g。 这个函数按顺序接收以下形参:

  • 箱中第一种产品的物品数量
  • 第一种产品每件物品的重量,单位为g
  • 箱中第二种产品的物品数量
  • 第二种产品每件物品的重量,单位为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. 获取报酬

你的报酬取决于搬了多少个箱子,以及卡车跑了多少趟。 每个箱子付你 5 美元,每趟付你 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 位非负整数
  • 每件丢失物品的价值,一个 64 位非负整数
  • 与你一起分摊报酬或债务的工人数量,一个 8 位正整数

示例:

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

返回值是 64 位整数。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
x86-64 Assembly Exercism

准备好开始 库存管理 了吗?

注册 Exercism,借助 22 个概念130 个练习 和真人导师指导,学习并掌握 x86-64 Assembly,全部免费。