汇编语言编程提示和技巧

9
我正在尝试编写自己的“玩具”操作系统,目前大部分使用汇编语言(NASM) - 部分原因是我希望它能帮助我理解x86反汇编,另一方面是因为我发现它相当有趣!这是我第一次使用汇编语言编程 - 我学得比我预期的要快,但是像学习任何显著不同的语言一样,我发现我的代码结构相当混乱,因为我试图弄清楚应该使用什么模式和约定。目前特别困难的是:跟踪寄存器。目前所有内容都在16位模式下运行,因此我只有6个通用寄存器可供使用,其中甚至更少可用于访问内存。我经常会覆盖自己的寄存器,这意味着我经常需要交换寄存器以避免这种情况 - 因此,即使有很多注释,我也很难跟踪寄存器包含哪些值。这正常吗?有没有什么可以做的来简化跟踪的事情?例如,我已经开始注释我所有的函数,并列出了被破坏的寄存器列表:
; ================
; c_lba_chs
; Converts logical block addressing to Cylinder / Head / Selector
;  ax (input, clobbered) - LBA
;  ch (output) - Track number (cylinder)
;  cl (output) - Sector number
;  dh (output) - Head number
; ================

保持栈的追踪
在一些情况下,当寄存器用完后我开始使用栈,但这只会使事情变得更糟——任何比简单的“push call pop”序列更复杂的东西都会让我完全失去追踪,使我难以甚至判断是否有正确数量的项目在栈上(特别是当涉及错误处理时——见下文),更不用说它们的顺序了。我知道一定有更好的使用栈的方法,但我就是想不出来。
处理错误
我一直在使用进位标志和零标志(取决于函数)来向调用者指示错误,例如:
myfn:
    ; Do things
    jz .error
    ; Do more things
    ret

    .error:
        stc
        ret

这是指示错误的正常方式吗?
另外,我能用什么其他提示或技巧来更好地构造我的汇编代码吗?
最后,有没有好的资源/写得很好的汇编示例?我找到了The Art of Assembly Language Programming,但它似乎非常注重语言的细节,对代码应该如何结构化的强调较少。(此外,一些代码示例使用段,我认为我应该避免使用)。
我正在使用零段(平面内存模型)完成所有这些工作,以保持简单,并在开始使用C时使事情变得更容易。

你可以使用内存来存储变量,不需要强制将所有内容都放入寄存器或堆栈中。 - Gunther Piez
1个回答

6
不要担心,你已经走在正确的道路上。作为汇编语言,你可以自由地决定如何管理寄存器和数据。我建议你为自己制定一些标准,使用类似于C的标准可能不是一个坏主意。我还建议你在第一个项目中使用不同的汇编语言(例如在qemu上运行的ARM),x86指令集有点可怕,但这是另一个话题...
汇编程序通常允许你声明变量,即使用名称的内存:
bob: .word 0x1234

然后从汇编语言(这里使用ARM汇编)开始

ldr r0,bob
add r0,#1
str r0,bob

寄存器是临时使用的,真正的数据保存在内存中。这种模型可以帮助跟踪事物,因为真正的数据与用户创建的变量名一样保存在内存中,就像高级语言一样。x86使这更容易,因为您可以对内存执行操作,而不必为每件事都经过寄存器。同样,您可以使用堆栈帧管理本地变量,从堆栈减去一些数字以覆盖该函数的堆栈帧,并在该函数内部知道/记住变量joe是堆栈指针+4,ted是堆栈指针+8等。可能在代码中使用注释来记住这些内容。记得在返回之前将堆栈指针/帧恢复到其入口点。这种方法有点困难,因为您没有使用变量名称而是使用数字偏移量。但是提供了本地变量和递归和/或一些全局内存节省。
作为人类使用您的眼睛和手(键盘和鼠标)进行此工作,您可能希望将数据保存在寄存器中的时间不超过文本编辑器屏幕上可以一次显示的内容,一目了然地看到变量转到寄存器,然后返回到内存中的变量,所有这些都可以一目了然。程序/编译器当然可以跟踪系统中的所有内存,远远超过人类。这就是为什么编译器平均生成比人类更好的汇编程序(特定情况下,人类始终可以调整或修复问题)。
错误处理,您需要小心使用标志,出于某种原因,它对我来说不太合适。它可能非常好,中断保留标志,您的代码将全部保留或设置标志等。嗯,标志的问题在于您必须在函数返回后立即检查/使用该返回值,而在您需要采样或使用该返回值之前有一条指令会修改标志。如果您使用寄存器,您可以选择在需要采样或使用该返回值之前不修改该返回寄存器的值多达许多个指令。
我认为这里的关键是,查看编译器使用的C调用约定规则,以及其他指令集,您将看到强烈的相似之处,有很好的理由。它们是可管理的。由于寄存器很少,您可以看到调用约定有时直接为所有参数和有时也为返回值使用堆栈。据说Amiga BIOS对每个BIOS函数都有一个自定义的调用约定,这使得系统执行紧凑而快速,但是尝试使用编译器重新创建BIOS或使用汇编器包装器附加到函数时,这是困难的。我相信如果没有每个函数的良好文档,这是不可管理的。在将来,您可能会决定希望使其可移植,并且希望选择一个常用的调用约定。您仍然需要注释代码以说明参数1是这个,参数2是那个等。另一方面,如果您当前或过去已经编写了x86汇编器调用DOS和BIOS调用,您将非常熟悉查找参考中的每个函数,并将数据放入每个函数的适当寄存器中。因为有很好的参考资料,因此每个函数都是定制的。

也许在局部变量中使用EQU是有意义的吗?你可以将EQU与局部标签一起使用吗? - ninjalj
我认为equ更像是C语言中的#define宏,它在第一次汇编通过时进行替换,不会分配内存。 - old_timer
我的意思是,如果你的本地变量 foo[EBP-8],你可以这样做 .foo EQU -8 - ninjalj
@ninjalj,啊,是的,我明白了,那是个好主意。 - old_timer
你能看一下这个吗?http://stackoverflow.com/questions/29708532/why-isnt-my-assembly-program-setting-r1-to-the-correct-value - committedandroider

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接