assembly 在程序集中将4位输入转换为十六进制

rmbxnbpk  于 2023-06-23  发布在  其他
关注(0)|答案(3)|浏览(136)

我试图在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

这是我尝试使用的代码

ldfqzlk8

ldfqzlk81#

以下是程序中的缺点列表以及改进它们的快速方法:

CR EQU 10
LF EQU 13

是反过来的!回车的ASCII码为13,换行符的ASCII码为10。

add8:
add word ptr inp2, 8

add4:
add word ptr inp2, 4

add2:
add word ptr inp2, 2

add1:
add word ptr inp2, 1

这目前有一个“失败”的问题,因为一旦进行了某个加法,可能随后的加法也会被执行,从而产生错误的结果。你可以通过插入跳转到 * conti * 标签,或者通过 * 使用 * fall-through以下面的无聊方式解决这个问题:

add8:
  add  word ptr inp2, 4   ; 4 + 2 + 1 + 1 = 8
add4:
  add  word ptr inp2, 2   ; 2 + 1 + 1 = 4
add2:
  add  word ptr inp2, 1   ; 1 + 1 = 2
add1:
  add  word ptr inp2, 1
  jmp  conti

比较/分支/加法链是一种糟糕的设置向量中的位的方法。我在底部包含的奖金计划做这件事要简单得多。要了解设置单个位的其他方法,请参阅下表中的问答。

sub ah, 30h
cmp ah, 1
je tes

DOS给你的字符在AL寄存器中。AH寄存器仍包含功能编号01h。因此,代码永远不会跳转到 * tes * 标签。你需要的是:

cmp  al, '1'     ; Let us assume the user only uses '0' and '1'
je   tes
conti:                                
  mov ah, 0
  sub al, 30h
  push ax
  mov ax, 10d
  mul bx
  pop bx
  add bx, ax

如果使用字节大小的乘法,这将变得简单得多:

conti:
  mov  ah, 10
  sub  al, '0'
  mul  ah
  add  bx, ax
end1:
  mov ax, bx
  mov dx, 0
  add ax, '0'  ; Convert the result to ASCII character
  mov dl, al
  mov ah, 02h
  int 21h

因为BX可能持有大于9的值,所以加上48的转换是不够的。由于您对十六进制输出感兴趣,并且该数字已经在BX中(这是8086上的地址寄存器),所有这些都设置为使用查找表:

end1:
  mov  dl, [hextab + bx]
  mov  ah, 02h
  int  21h

.data部分中添加下一行:

hextab db "0123456789ABCDEF"

虽然上面的修正不会神奇地把这个程序变成一个有效的解决方案,但有时你可以从实际尝试中看到你错在哪里学到很多东西。
Peter Cordes提供了一些相关问答的链接。如果你想在汇编编程方面做得更好的话,一定要阅读这些Q/A。
相关内容:

作为奖励,最完整的解决方案,此任务:

.model small
.stack
.data

msg  db  13, 10, 10, "Enter a number: $"
hex  db  "0123456789ABCDEF"

.code

START:
    mov  ax, @DATA
    mov  ds, ax
    
    mov  dx, OFFSET msg
    mov  ah, 09h
    int  21h
    
    xor  bx, bx    ; Decimal value of the binary input
    mov  cx, 8     ; Weight of the most significant digit (d3)
  More:
    mov  ah, 01h                      
    int  21h       ; -> AL
    cmp  al, 13
    je   Done      ; Early out, nice to not have to input trailing zeroes!
    sub  al, '0'
    cmp  al, 1
    ja   More      ; Redo for invalid input
    jb   IsZero
  IsOne:
    add  bx, cx
  IsZero:      
    shr  cx, 1     ; 8 -> 4 -> 2 -> 1 -> 0
    jnz  More      ; Max 4 digits
  Done:
    mov  dl, [hex + bx]
    mov  ah, 02h
    int  21h
    mov  ax, 4C00h ; Terminate the program
    int  21h
end START
sirbozc5

sirbozc52#

Sep的回答解释了问题中尝试的几个问题。
我认为这可能是有趣的展示一个简单和有效的方法的变化,一次获得输入1位数字,使用total = total*base + digit方法累积结果从最高有效数字开始,看看如何比较简单和效率。
此外,我看了看做2或4位的时间与移位或旋转,或乘法bithack。甚至在SSE 2的情况下一次16位。请参阅后面的部分答案,以及对变体和微优化的讨论,比如以not结尾,这样我们就可以在循环中构建以adc反转的位串。
这是基于Sep的“bonus”版本,重写了循环体。我简化了循环条件,使其在包括换行符在内的任何无效字符上退出,而不是忽略并继续在除换行符以外的任何字符上循环。我还删除了任何循环计数器;用户可以输入更多的数字,如果他们想在按回车之前,我们截断到最后4。

.model small
.stack
.data

msg  db  13, 10, 10, "Enter a base-2 number up to 4 bits long: $"
hex  db  "0123456789ABCDEF"

.code

START:
    mov  ax, @DATA
    mov  ds, ax

    mov  dx, OFFSET msg
    mov  ah, 09h
    int  21h

    xor  bx, bx     ; Integer value of the binary string input
    mov  ax, 0100h  ; zero AL, AH=01h = DOS char input
  More:
   ; append the previous bit / digit (0 on the first iteration instead of jumping over this)
    shl  bl, 1     ; bx =  (bx<<1) + CF  shift in the new digit
    or   bl, al
   ; get a new char and check it for being a base-2 digit. AH still set.
    int  21h       ; digit -> AL
    sub  al, '0'
    cmp  al, 1
    jbe  More      ; Exit on any invalid input, including newline
  Done:
    and  bx, 0Fh   ; truncate aka mod 16 in case the user entered a larger number
    mov  dl, [hex + bx]
    mov  ah, 02h
    int  21h
    mov  ax, 4C00h ; Terminate the program
    int  21h
end START

循环的效率基本上是无关紧要的,因为我们调用的是一个慢的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 + 1reg -= -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,bxxor-zeroing将让P6系列(PPro到Nehalem)CPU知道BX=BL,这样它们在以后阅读完整BX时就不会停止。但是,对完整BX的任何写入都会破坏该高位已知零的内部状态。只有P6系列将BL与BX分开重命名,因此其他微架构没有这种损失。第一代Sandybridge仍然可以将BL与EBX分开重命名,但不能将BX与EBX分开重命名,因此xor bx,bx在那里不是一个归零习惯用法。
cmcadd 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,bxadd 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,1mov ax, [si+2]配对。

mov   bx, [si]
          ;ror bh, 1  ;rol bx, 1 ; would be 4 uops on some CPUs
 shr   bh, 1         ; shift the bit we want into CF
 adc   bl, bl        ; shift it into BL.  2 uops on some CPUs, but not terrible.

 mov   ax, [si+2]     ; shift the next 2 bits into BL separately
 shr   ax, 1          ; first the low-address bit (also moving the last bit into AL)
 adc   bl, bl
 add   al, al         ; then shift the highest bit out the top of AL
 adc   bl, bl
 and   bx, 0x0f      ; P6 partial-register stall when reading BX after writing BL
                     ; P6 would prefer and al, 0xf / movzx bx, al  or something.

 mov dl,[Hex+bx]      ; lookup table, either ASCII digits or 7-seg codes

为了提高吞吐量并避免写入AH(这不是一个大问题,除非以后有东西读取AX,但可以使用Sandybridge系列上的另一个物理寄存器),我移动了整个AX并使用了add指令而不是另一个移位。(adcshr竞争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,1bt 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位

mov   eax, [si]
  and   eax, 0x01010101        ; saves 1 byte vs. loading into EBX
  imul  ebx, eax, 0x08040201   ; top byte = (low byte * 8) + (byte#1 * 4) + ...
  shr   ebx, 24             ; result = low 4 bits of EBX

  movzx edx, byte ptr [Hex+bx]   ; lookup if desired
    ; modern CPUs prefer writing a full reg, not merging a new DL into EDX

请参阅如何从8个bool值中创建一个字节(反之亦然)?- 回想一下,乘加输入的移位副本,如果这些加法不跨输入中的位域执行,则可以将其用作多移位并合并或相加。我们在这里反转乘数,在最高有效字节中使用0x08,将低字节的位放在高半字节的顶部,依此类推。在乘法的最高字节中结束的位是其位值加起来为24的位,因此高x低和低x高字节,以及两个中间字节。
我们可以通过这种方式将位打包成任意顺序,不需要提前使用bswap eax或任何东西,就像我们使用16位策略避免了ror ax, 8 2字节交换一样。对于MMX或SSE 2 SIMD(见下文),我们确实希望像pshufb这样的东西进行字节反转。

相关内容:

要使用SIMD检查无效字符,请参阅将C++中的字符串转换为大写字母的范围检查部分,但适用于'0' - '1'而不是'a'-'z')。处理第一个无效字符之后的数字,可能是范围检查结果上的pmovmskb eax, xmm0blsmsk。或者,由于BMI1 blsmsk这样的VX编码指令在真实的模式下不可用,所以使用mask ^= mask-1的等效比特ack,并将其与之进行AND运算,将压缩低位中的高位归零(即二进制整数)。(但这不会使对应于第一个非ASCII数字本身的位为零,所以我想将掩码右移1,或者使用一个bithack,给出掩码,但 * 不 * 包括最低的设置位。
此外,pmovmskb将把最低地址数字作为最低字节。但是它是第一个=最高的位置值,所以我们需要反转pmovmskb之前的向量,可能使用SSSE 3 pshufb。如果你只有一个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?所示

pb3skfrl

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)-我又看了一遍,这一点是错误的。
这是我的解决方案。我根本没有测试过它,所以可能仍然存在问题。

.model small
    .stack
    .data

    CR EQU 10
    LF EQU 13
    inp1 db CR, LF, CR, LF, "Enter a number: $"

    .code

    START:
        ;Prompt the user for a number
        MOV AX, @DATA
        MOV DS, AX
        
        mov dx, OFFSET inp1
        mov ah, 09h
        int 21h
        
        mov bx, 0
        mov cx, 0
        
    read:
        ;read input character into al
        mov ah, 01h
        int 21h
        
        ;quit if we found the null terminator
        cmp al, 0dh
        je end1

        ;increase our jump counter
        inc cx
        
        ;convert the input from a character to an integer
        sub al, 30h
        cmp ah, 1
        
        ;If the input was 0, no need to increment so read the next character
        jne read
        
        cmp cx, 1
        je add8

        cmp cx, 2
        je add4

        cmp cx, 3
        je add2

        cmp cx, 4
        je add1

        add8:
        add bx, 8
        jmp read
        
        add4:
        add bx, 4
        jmp read
        
        add2:
        add bx, 2
        jmp read
        
        add1:
        add bx, 1
        jmp read

    end1:
        ; put our converted number into ax and turn it to a character
        mov ax, bx
        add ax, '0'
        
        ;print out our number
        mov dx, 0
        mov dl, al
        mov ah, 02h
        int 21h
        
        ;Terminate the program
        mov ah, 4ch
        int 21h

    end START

编辑:一些方法来进一步改善这一点...如果我们在循环中循环,把bx向左移动,把我们的输入和1进行AND运算。然后将该值转化为bx。

.model small
    .stack
    .data

    CR EQU 10
    LF EQU 13
    inp1 db CR, LF, CR, LF, "Enter a number: $"

    .code

    START:
        ;Prompt the user for a number
        MOV AX, @DATA
        MOV DS, AX

        mov dx, OFFSET inp1
        mov ah, 09h
        int 21h

        mov bx, 0

    read:
        ;read input character into al
        mov ah, 01h
        int 21h

        ;quit if we found the null terminator
        cmp al, 0dh
        je end1

        ;convert the input from a character to an integer
        sub al, 30h

        ;clear all the bits but the LSB (undefined behaviour if the user entered anything but 0 or 1)
        and al, 1

        ;shift left, and OR in the new lsb
        shl bx, 1
        or bx, ax
        
        ;read in our next character
        jump read

    end1:
        ; put our converted number into ax and turn it to a character
        mov ax, bx
        add ax, '0'

        ;print out our number
        mov dx, 0
        mov dl, al
        mov ah, 02h
        int 21h

        ;Terminate the program
        mov ah, 4ch
        int 21h

    end START

相关问题