由于此问题涉及少量数字(尤其是循环计数为100的数字),因此可以通过简单地使用16位和8位寄存器来简化模运算设置: {\ text {[AX](16位寄存器)}} {\ text {[其他8位寄存器]}} = \ text {[AH](其余)} $$

我的主要关注的是布局。我见过的每个“基本”高级实现都有一个检查并针对每种情况一起打印。我发现在这里做同样的事情比较容易,但是我不确定这在汇编中是否也不太可读。 。不幸的是,我仍然会在每种情况下都这样做,因为我要增加一个寄存器(CX)并使用另一个寄存器(AX)进行分红。对于这两者,我都可以坚持使用AX,但这可能涉及保留当前计数器值的副本,这可能会使代码更加复杂。我想无论如何这不是什么大问题。

使用的宏:



nwln-打印换行符> PutStr-打印定义的字符串

PutInt-打印16位整数值

不必寻址宏;它们确实可以正常工作。

%include "macros.s"

.DATA

fizz_lbl:       DB    "Fizz", 0
buzz_lbl:       DB    "Buzz", 0
fizzbuzz_lbl:   DB    "FizzBuzz", 0

.CODE
.STARTUP

xor   CX, CX ; counter

main_loop:
   inc   CX
   cmp   CX, 100

   jg    done

   fizzbuzz_check:
      mov   AX, CX ; dividend = counter
      mov   BH, 15 ; divisor
      div   BH     ; (counter / 15)

      cmp   AH, 0            ; counter divisible by 15?
      je    print_fizzbuzz   ; if so, proceed with printing

      jmp   fizz_check       ; if not, try checking for fizz

      print_fizzbuzz:
          PutStr   fizzbuzz_lbl
          nwln
          jmp      main_loop

   fizz_check:
      mov   AX, CX ; dividend = counter
      mov   BH, 3  ; divisor
      div   BH     ; (counter / 3)

      cmp   AH, 0        ; counter divisible by 3?
      je    print_fizz   ; if so, proceed with printing

      jmp   buzz_check   ; if not, try checking for buzz

      print_fizz:
          PutStr   fizz_lbl
          nwln
          jmp      main_loop

   buzz_check:
      mov   AX, CX ; dividend = counter
      mov   BH, 5  ; divisor
      div   BH     ; (counter / 5)

      cmp   AH, 0        ; counter divisible by 5?
      je    print_buzz   ; if so, proceed with printing

      jmp   print_other  ; if not, then can only display number

      print_buzz:
          PutStr   buzz_lbl
          nwln
          jmp      main_loop

   print_other:
      PutInt   CX
      nwln
      jmp   main_loop

done:
   .EXIT


评论

真好! +1是努力编写这门低级语言。我可以建议发布结果集吗?

@Phrancis:您要验证成功执行吗?

是的,基本上。尽管我毫不怀疑它执行了。

@Phrancis:我只是犹豫,因为这会占用很多(可能是不必要的)空间。截图的链接就足够了吗?

在哪里可以获取macros.s?

#1 楼

由于我们使用汇编语言进行此操作,因此比高级语言通常更有效地执行此操作是有意义的。否则,为什么还要麻烦汇编语言?这样说来,有很多方法可以使它效率更高得多。既然我们已经知道我们正在寻找可以被3、5或两者整除的数字,那么更有意义的是简单地为这两个数字保留倒数计数器。您的初始化当前显示为:

    xor cx, cx


可以很容易地扩展为:

    xor cx, cx
    mov bx, 0503h  ; set bh = 5 counter, bl = 3 counter


然后代替除法,简单地递减:

    inc cx
    cmp cx, 100
    jg done
;  instead of this...
;    dec bh
;    dec bl
;    cmp bx, 0
; per suggestion from @Chris Jester-Young use this:
    sub bx, 0101h
    je print_fizzbuzz
    cmp bl, 0
    je print_fizz
    cmp bh, 0
    je print_buzz
print_other:


自然地,各种div例程将不得不重置print_...bh或同时重置这两者以及打印。

改进格式化

通常来说,汇编语言代码不会以缩进代码的方式缩进。它线性得多,只有汇编语言语句或指令的缩进形式。使数字输出保持字符串形式,增加每个ASCII数字并发出字符串的效率更高,而不是将二进制寄存器的内容重复转换为字符串值。

评论


\ $ \ begingroup \ $
代替dec bh; dec bl,您只能使用sub bx 101h。然后,您可以直接jz print_fizzbuzz,而无需cmp。其他比较可以使用测试bl,bl和测试bh,bh来消除代码中的文字0(这使代码更小)。
\ $ \ endgroup \ $
–克里斯·杰斯特·杨(Chris Jester-Young)
15年7月13日在11:10

\ $ \ begingroup \ $
@ ChrisJester-Young所有出色的建议。谢谢!
\ $ \ endgroup \ $
–爱德华
15年7月13日在11:53

\ $ \ begingroup \ $
有趣的黑客为此使用了部分寄存器,特别是在避免写入一半寄存器之后避免读取更宽的寄存器的方式(除非仅重置其中一个计数器)。这将导致P6系列上的部分寄存器停顿。 (为什么GCC不使用部分寄存器?)。另一种选择是按3展开,并且仅使用一个减计数器,就像我在此FizzBu​​zz答案中使用的那样,我将字节存储在缓冲区中以进行打印。
\ $ \ endgroup \ $
– Peter Cordes
20 Dec 20 '20:27

\ $ \ begingroup \ $
test reg,reg优于cmp reg,0-它短1个字节,并且在所有情况下(AF除外)都完全相同地设置FLAGS。用CMP reg,0和OR reg,reg测试寄存器是否为零?
\ $ \ endgroup \ $
– Peter Cordes
20/12/21在23:29

#2 楼

使用本地标签
您所有的标签都是全局标签。
由于所有这些标签都试图完成相同的任务,并且它们都可以一起使用,因此应将它们全部归为一个全局变量其余标签是本地标签。例如,您可以更改以下标签:
fizzbuzz_check:

.fizzbuzz_check:

这也是更好的做法。

不同的条件跳转
在每次检查Fizz,Buzz或FizzBu​​zz时,您都可以执行以下操作:
  je    print_fizzbuzz   ; if so, proceed with printing

  jmp   fizz_check       ; if not, try checking for fizz

  print_fizzbuzz:

可以缩短为:
  jne    main_loop

  print_fizzbuzz:

如果没有通过jne,执行将落入print_fizzbuzz

通用性
现在,您的代码仅支持Fizz,Buzz和Fizzbuzz。
但是,如果您想稍微改变一下怎么办?假设您想每第四个数字都说“嘶嘶响”?
要这样做,您需要添加大量代码。
有一种更简单的方法;使用struc
假设您创建了struc
struc message

    .say: resb 10
    .num: resb 1

endstruc

然后您可以执行以下操作轻松创建一堆消息:
messages:
    db "FizzBuzz", 0, 0
    db 15

    db "Buzz",0,0,0,0,0,0
    db 5

    db "Fizz",0,0,0,0,0,0
    db 3

    db 0,0,0,0,0,0,0,0,0,0; so, when iterating, can know if the end has been reached
    db 0

(多余的0是用于填充名称所给定的10个字节)
(请注意顺序:您希望最大到最小)
现在,您可以轻松地
在主代码中,遍历messages,如果计数器可以被num字段中的值整除,则登录say字段。
现在,代码可以这样写:
xor cx, cx

main_loop:
    inc cx
    cmp cx, 100

    jg .done

    call search

    jmp main_loop

.done:
    .EXIT

search:
    mov si, messages

.next:
    mov ax, cx
    mov bh, [si + message.num]; divisor
    div bh

    cmp ah, 0; was evenly divisible
    je .print_message

    add si, message_size
    cmp byte [si], 0; the next item in `messages` is the terminator
    jne .next

    jmp .print_num

.print_message:
    PutStr [si + message.say]
    nwln
    ret
    
.print_num:
    PutInt cx
    nwln
    ret

注意:在没有macros.s的情况下进行测试很麻烦,因此如果有任何问题,请通知我

评论


\ $ \ begingroup \ $
消息:\ .fizzbuzz:db“ FizzBu​​zz”,0 \乘以10-($-.fizzbuzz)db 0对于将字符串字段填充到十个字节长更好。
\ $ \ endgroup \ $
– ecm
19/12/10在17:55

\ $ \ begingroup \ $
如果使用.num == 0作为终止符,则每次迭代仅需加载一次(在第一次迭代之前加载第一次迭代的除数,即旋转并部分剥离循环以实现此目的)。或使用当前代码,div字节[si + message.num]避免浪费将其加载到BH中的指令。另外,您可以将.print_num放在.print_message之前,这样循环就可以陷入其中。主循环也可以使用更有效的do {cx ++; } while(cx <100);循环结构:为什么循环总是编译成“ do ... while”样式(尾跳)?
\ $ \ endgroup \ $
– Peter Cordes
20 Dec 20'20:39



\ $ \ begingroup \ $
搜索中的代码不必是一个函数,即使它很大,也可以只是循环体。这可能不太可读,但是如果要最大程度地提高可读性,请使用编译器。通常,使代码更通用是在asm中实现时要做的最后一件事:尽可能利用特殊情况。再一次,否则使用编译器,以便它可以即时利用固定常量(例如2和4为2的幂),除非代码真正需要处理除数和消息的运行时变量数组,否则它不使用div。
\ $ \ endgroup \ $
– Peter Cordes
20 Dec 20 '20:43