【问题标题】:Woes of implementing selection sort in x86 NASM or YASM Assembly在 x86 NASM 或 YASM 程序集中实现选择排序的麻烦
【发布时间】:2012-04-29 07:35:13
【问题描述】:

我正在尝试在 NASM 中实现一个在 64 位 Linux 上运行的数组的选择排序。

数组声明为:

section .bss
strbuf resb 10
small  resb  1  ; current minimum value

排序算法本身很简单,但我觉得受到两件事的限制:可用寄存器的数量,以及无法交换立即数(即括号)

我需要跟踪未排序的数组边界、它的索引、当前最小值的位置以及它的索引。那已经是四个寄存器了。我还需要跟踪两个循环计数器,一个用于表示未排序数组边界的外部循环,另一个用于每次遍历的迭代(即内部循环)。那是另外两个,总共六个寄存器。

由于立即数不能相互移动,例如mov [var1], [var2],所以每当我需要交换两个元素时,我都需要使用寄存器作为临时占位符。在跟踪哪些寄存器保存哪些信息方面,这很快变得笨拙!

以下是我目前的尝试。请注意,这是触发分段错误的无效代码。但也许你能看出我在做什么,并指出我的方法哪里出了问题。

我不希望使用简化结构的宏,例如提供 .IF 和 .ELSE 的宏。

; ====== Sorting begins here ======
sorting:
    mov edi, strbuf  ; outer loop pointer
    mov esi, strbuf  ; inner loop pointer
    mov eax, 0  ; inner loop counter
    mov ebx, 0  ; outer loop counter

innerloop:
    ; store the value of first element in [small]
    mov edx, [esi]
    mov [small], edx

    ; compare the current small value with the value pointed by esi
    mov edx, [esi]  
    cmp [small], edx

    jg  new_small
    inc esi
    inc eax
    cmp eax, 9
    jle innerloop
    cmp eax, 9
    jg  innerloop_done

new_small:
    mov [small], edx  ; save the new small value
    mov ecx, esi      ; save its index in ecx
    inc esi
    inc eax
    cmp eax, 9

    jle     innerloop

innerloop_done:
    ; When the inner loop is completed...
    ; First, do the swap
    push    rax
    mov eax, [edi]
    mov edx, [small]
    mov [ecx], edx
    pop rax

    inc edi  ; move the outer loop pointer forward
    inc esi  ; move the inner loop pointer forward
    inc ebx  ; increment the outer loop counter (the unsorted array becomes smaller)
    inc eax  ; increment the inner loop counter (same reason as above)
    cmp ebx, 9

    jle innerloop   

; ====== Sorting ends here ======

【问题讨论】:

  • 关于如何正确构建汇编代码的指南似乎并不多。每当您阅读有关 C++、C 或 Java 等流行语言的编程书籍时,您总是会获得扎实的、扎实的编程原则,以防止您自欺欺人。在汇编中,唯一的原则似乎是“只要你需要让你的程序正常工作,就在脚上开枪。”
  • :)) ASM 非常需要注意力,需要大量细致而乏味的工作。但!它在某一方面优于 C 和 C++。它是固定的。当您使用不同的汇编程序时,x86 字节、字和双字的大小不会改变。并且 x86 ASM 不能直接适用于 ARM 或 MIPS 等其他平台或可重新编译。没有来自 C/C++ 程序员很少阅读和理解的语言标准的未定义行为。 CPU 手册准确地说明了该 CPU 的有效内容和无效内容。所见即所得! :)
  • 您也可以在 ASM 中使用许多高级编程技术。您可以定义子例程,可以定义宏,所有这些都不要重复自己并避免一些错误。您可以评论代码。您可以在多个模块中构建它。您可以拥有回调和其他可间接调用的子例程。虚拟方法,如果你愿意的话。在 ASM 中,您可以做任何事情,只是需要更多的代码和工作。
  • 谢谢。我还发现很少有高级编程概念有助于汇编。我的思维习惯于将数据视为值、指针或地址。但在 ASM 中,这些概念似乎都不适用,因为寄存器不知道它包含什么类型的数据。
  • ASM 本质上是极其明确和低级的。没有隐藏的函数调用、跳转、堆栈展开、类型转换、临时对象。我发现了解 ASM 对学习 C 和 C++ 有很大帮助。反过来说,没那么多。这是有原因的。 C 和 C++ 是从 ASM 发展而来的。

标签: assembly x86 x86-64 nasm yasm


【解决方案1】:

如果这应该在 64 位模式下执行,则必须使用 64 位地址,这意味着当您获取这些地址并将它们放入寄存器时,这些接收者寄存器也必须是 64 位的,否则您'将截断地址并访问不完全符合您预期的内存。

另外,您没有调试器来单步调试您的代码吗?

【讨论】:

  • 除了gdb之外,还有什么好的Linux汇编调试器吗? gdb 似乎不能很好地处理汇编代码。
  • 我想我一直在使用 32 位寄存器,因为这就是书中示例所使用的。由于程序只处理字符,我稍后在程序中也使用 8 位寄存器。我的印象是处理字符时应该使用AL等8位寄存器?
  • 如果您一次处理一个字符,8 位寄存器/变量是完美的。如果您想一次处理多个,则需要更大的寄存器/变量。至于 gdb,我不在 Linux 上,但也必须可以用它来调试 ASM 程序。查看是否需要使用命令行参数在 NASM 或链接器(如果有的话)中启用调试/符号信息。这可能会有所帮助。否则,即使是像上面这样的小程序,查看反汇编而不是查看源代码并拥有所有变量/子例程名称应该没什么大不了的。
【解决方案2】:

对于 64 位代码,有 16 个通用寄存器:RAX、RBX、RCX、RDX、RSI、RDI、RSP、RBP、R8、R9、R10、R11、R12、R13、R14、R15。

其中,RSP 有一个特殊用途,并且只能用于该用途(当前堆栈地址)。 RBP 寄存器通常由编译器用于跟踪堆栈帧(不包括“-fomit-frame-pointer”可能性),但您不是编译器,可以将它用于任何您喜欢的事情。

这意味着在您可以使用的 15 个寄存器中,您只使用了其中的 6 个。

如果您确实确实用完了寄存器,那么您可以将一些东西转移到堆栈空间。例如:

foo:
    sub rsp,8*5        ;Create space for 5 values
%define .first   rsp
%define .second  rsp+8
%define .third   rsp+8*2
%define .fourth  rsp+8*3
%define .fifth   rsp+8*4
%define .first   rsp+8*5

    mov [.first],rax
    mov [.second],rax
    mov rax,[.first]
    add rax,[.second] 
    mov [.third],rax
...
    add rsp,8*5        ;Clean up stack
    ret

希望您可以看到堆栈上可能有数百个值,并在需要时使用一些寄存器(临时)保存这些值。通常你会计算出最常用的值(例如在内部循环中),并尝试为它们使用寄存器并将堆栈用于最不常用的变量。但是,对于 64 位代码(您可以使用另外 8 个寄存器),很少会出现寄存器用完的情况,如果您这样做了,这可能表明您需要将例程拆分为多个例程。

【讨论】:

  • 谢谢 Brendan,我不知道寄存器 r8-r15。在课堂上或任何学习材料中都没有提到它们。根据我的发现,它们似乎不存在于 32 位 x86 中。当您使用add esp, 8*5 清理堆栈时,这是否会完全使之前的所有寄存器分配无效?移动esp 对事物有何影响?如果我使用sub esp, 8*5 向后移动堆栈指针会怎样?
  • @Terribad:你真的需要开始阅读 Intel/AMD CPU 文档。它会告诉您有多少个寄存器、可用的指令以及它们的确切作用。
  • @Terribad:除了在 (e/r)sp 中获得不同的值之外,更改堆栈指针还有一个简单但非常重要的效果。地址小于 (e/r)sp 的所有堆栈上的数据实际上是无效的,因为它可以被中断服务例程、信号处理程序等的活动覆盖,因为毫无疑问,它们也需要使用堆栈。如果您将 rsp 增加太多然后恢复它,您可能会丢失存储在堆栈中的返回地址。当然,栈上参数和局部变量也是一样的。
猜你喜欢
  • 2012-11-03
  • 1970-01-01
  • 1970-01-01
  • 2015-09-20
  • 2021-11-15
相关资源
最近更新 更多