我试图在TASM 1.4程序集中获取一个4位数的输入,并将其转换为相应的十六进制值,但当我转换它时,我得到的输出错误,例如,输入1011
,我希望返回的值是11,但我得到的只是'#'或'['
.model small
.stack
.data
CR EQU 10
LF EQU 13
inp1 db CR, LF, CR, LF, "Enter a number: $"
inp2 dw 0
count db 0
.code
tes:
cmp count, 1
je add8
cmp count, 2
je add4
cmp count, 3
je add2
cmp count, 4
je add1
add8:
add word ptr inp2, 8
add4:
add word ptr inp2, 4
add2:
add word ptr inp2, 2
add1:
add word ptr inp2, 1
jmp conti
START:
MOV AX, @DATA
MOV DS, AX
mov dx, OFFSET inp1
mov ah, 09h
int 21h
mov bx, 0
start1:
mov ah, 01h
int 21h
cmp al, 0dh
je end1
inc count
sub ah, 30h
cmp ah, 1
je tes
conti:
mov ah, 0
sub al, 30h
push ax
mov ax, 10d
mul bx
pop bx
add bx, ax
jmp start1
end1:
mov ax, bx
mov dx, 0
add ax, '0' ; Convert the result to ASCII character
mov dl, al
mov ah, 02h
int 21h
mov ah, 4ch ; Terminate the program
int 21h
end START
这是我尝试使用的代码
3条答案
按热度按时间ldfqzlk81#
以下是程序中的缺点列表以及改进它们的快速方法:
是反过来的!回车的ASCII码为13,换行符的ASCII码为10。
这目前有一个“失败”的问题,因为一旦进行了某个加法,可能随后的加法也会被执行,从而产生错误的结果。你可以通过插入跳转到 * conti * 标签,或者通过 * 使用 * fall-through以下面的无聊方式解决这个问题:
比较/分支/加法链是一种糟糕的设置向量中的位的方法。我在底部包含的奖金计划做这件事要简单得多。要了解设置单个位的其他方法,请参阅下表中的问答。
DOS给你的字符在AL寄存器中。AH寄存器仍包含功能编号01h。因此,代码永远不会跳转到 * tes * 标签。你需要的是:
如果使用字节大小的乘法,这将变得简单得多:
因为BX可能持有大于9的值,所以加上48的转换是不够的。由于您对十六进制输出感兴趣,并且该数字已经在BX中(这是8086上的地址寄存器),所有这些都设置为使用查找表:
在
.data
部分中添加下一行:虽然上面的修正不会神奇地把这个程序变成一个有效的解决方案,但有时你可以从实际尝试中看到你错在哪里学到很多东西。
Peter Cordes提供了一些相关问答的链接。如果你想在汇编编程方面做得更好的话,一定要阅读这些Q/A。
相关内容:
作为奖励,最完整的解决方案,此任务:
sirbozc52#
Sep的回答解释了问题中尝试的几个问题。
我认为这可能是有趣的展示一个简单和有效的方法的变化,一次获得输入1位数字,使用
total = total*base + digit
方法累积结果从最高有效数字开始,看看如何比较简单和效率。此外,我看了看做2或4位的时间与移位或旋转,或乘法bithack。甚至在SSE 2的情况下一次16位。请参阅后面的部分答案,以及对变体和微优化的讨论,比如以
not
结尾,这样我们就可以在循环中构建以adc
反转的位串。这是基于Sep的“bonus”版本,重写了循环体。我简化了循环条件,使其在包括换行符在内的任何无效字符上退出,而不是忽略并继续在除换行符以外的任何字符上循环。我还删除了任何循环计数器;用户可以输入更多的数字,如果他们想在按回车之前,我们截断到最后4。
循环的效率基本上是无关紧要的,因为我们调用的是一个慢的
int 21h
I/O函数,但是如果我们从一个具有类似循环的数组中阅读,那就很有趣了。以字节为单位的小机器代码总是很好的,特别是对于回溯计算或I-cache占用空间。循环条件为range-check,输入为
'0'-'1'
。因此,当我们返回顶部时,根据cmp al, 1
设置FLAGS,其中AL为0(ZF=0,CF=1)或1(ZF=1,CF=0)。超出该范围的输入字节导致(ZF=0,CF=0),因此jbe
不被采用。由于该范围只包含2个整数,所以通过使用
cmp al, 1
/jbe
而不是cmp al, 2
/jb
(又名jc
)使顶部的一个特殊,我们留下的CF设置与我们想要附加到我们累加的总数的位值相反(CF=!AL),以及排除该范围之外的所有值。正如Sep在评论中指出的那样,
cmc
/adc bx,bx
将是使用CF值而不是AL值与shl
/or
的一种方式。这可以保存1字节的代码大小(或者2字节,因为我们可以使用mov ah, 1
而不是mov ax, 0100h
)。但是我们当前的代码福尔斯了
xor bx,bx
的CF=0的循环中,因此total = total*base + digit
代码将以digit=!CF=1。我们要么需要用一个jmp
进入循环,得到一个新的字符(跳过cmp/adc),要么在进入循环之前需要一个额外的stc
,或者部分剥离第一次迭代,阅读一个输入字符并分支到跳过或进入循环。在当前循环中,我们使用AL代替CF表示数字,因此运行digit=0的
total = total*base + digit
代码没有效果。(我们可以将AL归零,以获得1个额外字节的代码大小,但不获得额外指令,这是设置AH= 01 h的一部分。)对于代码高尔夫(无论效率如何,最小代码大小),我们可以在循环外使用stc
,在循环外使用cmc
/adc bx,bx
,以节省1个字节的代码大小,代价是在循环外多执行一条指令,并使用adc
,这在Intel上较慢(3 uops:https://agner.org/optimize/)从P6到Broadwell。我们想要附加的位值在ZF中也是非反相的,但只有CF有特殊指令将其添加(adc/sbb)或移位(rcl/rcr)到寄存器中。使用386
setz al
,我们可以将ZF重新具体化到寄存器中,但这将是愚蠢的,因为我们在AL中已经有0或1位数的值。你可以用CF做的其他有趣的事情包括添加
sbb reg, -1
!CF(reg -= -1 + 1
或reg -= -1 + 0
),但如果不首先移位bx
,这在这里是没有用的,而且不修改FLAGS也不方便。在循环条件设置FLAGS之前也不方便。根据AL设置CF!= 0可以用
add al, 255
(2字节)完成,但add al, 255
/adc bx,bx
并不比简单的shl bl,1
/or bl, al
好。**我们可以从BX=-1开始,然后在
and bx, 0Fh
之前执行not
。这将允许仅在顶部的adc bx,bx
将CF附加到BX,建立一个反转的位模式,我们在最后翻转。从BX=全1(0xFFFF = 2的补码-1
)开始与从BX=0开始并插入非反相位相同。第一次迭代仍然需要进入CF=1或跳过bx = (bx<<1)+CF
部分,例如使用stc
。如果我们有一个数字0扩展到AX中(如果我们在数组上循环的话很容易),我们可以使用386条指令,比如
lea bx, [ebx*2 + eax]
,因为32位寻址模式有a 2-bit shift count for the index,并且不受可以使用哪些寄存器的限制。我们也可以使操作数大小为32位,以处理长达32位的输入。性能,P6等半现代CPU上的部分寄存器
使用conditional branch at the bottom of the loop意味着总共只需要一个分支,这通常在代码大小和性能方面都更好,尽管这与I/O循环基本无关。处理第一次迭代的方法包括剥离初始输入和检查(在进入循环之前执行第一个char输入),或者在底部的input+条件中执行
jmp
,或者我在这里所做的,安排初始状态,以便我们可以“落入”循环并添加零。在循环内部,我可以移位
shl bx, 1
,而不仅仅是bl
,这将允许相同的循环对最多16位整数的输入起作用,而不会截断它们。但是,在用or bl, al
写入低8位后,读+写16位寄存器将创建partial-register stall on older Intel CPUs。但是or bx, ax
也会造成部分寄存器暂停阅读AX,更重要的是会从AH=1或非零高位。(We假设
int 21h
处理程序在某个时刻推送和弹出BX,从而写入完整的寄存器,那么通过写入BL,您可能会创建一个部分寄存器停顿。如果不是这样,循环前的xor bx,bx
xor-zeroing将让P6系列(PPro到Nehalem)CPU知道BX=BL,这样它们在以后阅读完整BX时就不会停止。但是,对完整BX的任何写入都会破坏该高位已知零的内部状态。只有P6系列将BL与BX分开重命名,因此其他微架构没有这种损失。第一代Sandybridge仍然可以将BL与EBX分开重命名,但不能将BX与EBX分开重命名,因此xor bx,bx
在那里不是一个归零习惯用法。cmc
或add al, 255
(设置CF = AL!=0)/adc bx,bx
可以在没有部分寄存器停顿的情况下工作,但是adc reg,reg
在Broadwell之前的Intel上是2个uops。(所以是rcl bx, 1
;根据https://uops.info/,即使在像Skylake这样的后期CPU上也有3个uops,尽管在AMD Zen上只有1个。由于P6系列的部分寄存器考虑,如果我想累积16位或32位值,我可能会使用
cmc
/adc bx,bx
或add al,255
/adc bx,bx
,如果我关心这些CPU而不仅仅是8086。CMC在最初的8086(timing tables for 8088 through 586,不考虑代码提取瓶颈)上速度很快,在现代x86上仍然高效,单uop具有1个周期的延迟。使用它而不是AL直接使从AL获得ASCII数字的延迟链长1个周期(sub/cmp/cmc/adc vs. sub/add),但这不是通过BX的循环承载依赖链的一部分。事实上,在具有高效
adc
的CPU上,这更好,因为从旧BX到新BX的adc bx,bx
是1个周期延迟。而shl bl,1
/or bl,al
是两个操作的链。加载2字节-> 2位字符串,或4字节的乘法位
考虑一个例子,我们在内存中有一个字符串,我们提前知道数组/字符串的长度,所以我们不必单独检查每个数字是否超出范围。
我们可以一次加载2个字节,然后将这些位混洗在一起。对于字符串中按打印顺序排列的二进制数字(最高有效位优先,位于最低地址),并且x86是little-endian,2字节加载将具有与我们想要的相反顺序的位。所以我们实际上需要将上半部分的低位移动或旋转到下半部分。
Sep在评论
ror ah, 1
/rol ax, 1
中建议首先从第2个字节(AH)到寄存器顶部获取我们想要的位,然后将其旋转到AL底部的位旁边。这在现代英特尔CPU(Sandybridge系列)上并不理想,其中rotate-by-1的成本为2 uops(https://uops.info/),因为它必须更新FLAGS,留下一些未修改的内容,这与rotate by immediate不同。它将有部分寄存器摊位的P6家庭(PPro通过Nehalem)。8086兼容代码,在所有CPU上运行正常,尽管对其中一些CPU来说并不完美。
rol
by 1是P6系列上的单微操作,adc
是2,但在写入较窄的寄存器部分后阅读较宽的寄存器时,它们会在多个周期内出现部分寄存器停顿。对于P5-奔腾(顺序双问题),这可以被安排为至少允许shr bh,1
与mov ax, [si+2]
配对。为了提高吞吐量并避免写入AH(这不是一个大问题,除非以后有东西读取AX,但可以使用Sandybridge系列上的另一个物理寄存器),我移动了整个AX并使用了
add
指令而不是另一个移位。(adc
和shr
竞争Haswell上的端口0和6。)“更简单”的版本将是shr al, 1
/.../shr ah, 1
,它在理论上具有指令级并行性,并且在实践中在P6系列上具有指令级并行性,P6系列将AL与完整的EAX分开重命名。许多替代方案是可能的,特别是对于386指令,例如准备BX,然后
ror ah, 1
/ror ax, 1
/shld bx, ax, 2
将2位从AX的顶部移到BX的底部。但SHLD在现代AMD(Zen)上运行缓慢。我找不到一种在P6系列上使用旋转而不使用多个部分寄存器的方法,所以adc
似乎是最不坏的。任何实际用例的最佳选择都取决于您关心的CPU。而不是shr bh,1
,bt bx, 8
将把位放入CF,而不会在EBX中产生部分寄存器问题。在Intel CPU(如Core 2)上完全高效,在AMD Zen上有2个uops。如果我们想使用
xlat
,我们不需要扩展到16位,也许可以避免P6上的部分寄存器停顿。或者使用386条指令,获取两个单独的2位值,屏蔽高垃圾,并与lea bx, [ebx + eax*4]
合并要获得其他顺序,
ror al, 1
/shr ax, 7
是可能的,但在Intel P6系列上有部分寄存器延迟。(386和更高版本具有桶形移位器,用于有效移位超过1。)使用快速乘法ALU(如P6及更高版本),甚至可以一次4位
请参阅如何从8个bool值中创建一个字节(反之亦然)?- 回想一下,乘加输入的移位副本,如果这些加法不跨输入中的位域执行,则可以将其用作多移位并合并或相加。我们在这里反转乘数,在最高有效字节中使用
0x08
,将低字节的位放在高半字节的顶部,依此类推。在乘法的最高字节中结束的位是其位值加起来为24的位,因此高x低和低x高字节,以及两个中间字节。我们可以通过这种方式将位打包成任意顺序,不需要提前使用
bswap eax
或任何东西,就像我们使用16位策略避免了ror ax, 8
2字节交换一样。对于MMX或SSE 2 SIMD(见下文),我们确实希望像pshufb
这样的东西进行字节反转。相关内容:
shr al,1
设置CF = AL低位的计数循环,即将低位从AL移出到CF。rcl bx,1
将其转移到BX。它适用于超过4位宽的数字,因为它的整数到十六进制函数在4位组上使用循环,如果需要,可以生成多个十六进制数字。1111
(F
为十六进制,15
为十进制)。要有效地处理较长的二进制整数,请使用SSE 2 SIMD:* Does the x86 architecture support packing bools as bits to parallelize logic operations? * / * Extract the low bit of each bool byte in a __m128i? bool array to packed bitmap * - load,然后
pslld xmm0, 7
将低位移到字节的顶部,然后SSE 2pmovmskb
一次从多个字节的每个字节中打包一个位。'0'
的低位为0,'1'
的低位为1
。要使用SIMD检查无效字符,请参阅将C++中的字符串转换为大写字母的范围检查部分,但适用于
'0' - '1'
而不是'a'-'z'
)。处理第一个无效字符之后的数字,可能是范围检查结果上的pmovmskb eax, xmm0
和blsmsk
。或者,由于BMI1blsmsk
这样的VX编码指令在真实的模式下不可用,所以使用mask ^= mask-1
的等效比特ack,并将其与之进行AND运算,将压缩低位中的高位归零(即二进制整数)。(但这不会使对应于第一个非ASCII数字本身的位为零,所以我想将掩码右移1,或者使用一个bithack,给出掩码,但 * 不 * 包括最低的设置位。此外,
pmovmskb
将把最低地址数字作为最低字节。但是它是第一个=最高的位置值,所以我们需要反转pmovmskb
之前的向量,可能使用SSSE 3pshufb
。如果你只有一个4位整数,mov eax, [si]
/bswap eax
/movd xmm0, eax
也可以工作。从
pmovmskb
得到一个16位整数(或从使用shift/OR组合结果得到32位整数)后,可以使用更多SIMD代码来生成一个十六进制字符串来表示它,如How to convert a binary integer number to a hex string?所示pb3skfrl3#
很多小问题。
1.你的字符输入是al,但你用ah减去30 h
1.你在你的tes标签中失败了-所以如果你添加了8,你也添加了4,2和1。(加4加2加1),加2加1...)
1.你从左到右读取输入(msb到lsb),但把它们加到你的数字lsb到msb中。(例如1000会变成1,0001会变成8)-我又看了一遍,这一点是错误的。
这是我的解决方案。我根本没有测试过它,所以可能仍然存在问题。
编辑:一些方法来进一步改善这一点...如果我们在循环中循环,把bx向左移动,把我们的输入和1进行AND运算。然后将该值转化为bx。