获得汇编语言编程技能

9

我是一名DSP、嵌入式软件程序员,希望提高我的汇编语言编程技能。在我的职业生涯中,我已经使用C、Matlab和少量汇编语言编程(ARM汇编、DSP处理器汇编)达七年之久。

现在,我想通过大量的学习来将我的汇编语言编程技能(可以是任何汇编语言)提升到“专家级别”。我知道更多的编程实践是实现这个目标的途径,但我在这里要问的是:

  • 人们在汇编语言编程方面的经验(任何汇编语言),他们通过多年的编程实践获得了哪些经验?

  • 在学习新的汇编语言时需要注意什么?

  • 编写高效和正确的汇编语言代码的具体技巧和方法?

  • 如何将给定的C代码有效地转换成最优汇编代码?

  • 如何清晰地理解给定的汇编代码?

  • 如何跟踪寄存器中的操作数、堆栈指针、程序计数器等信息,以更好地了解底层架构和为程序员提供的资源等?

基本上,我想从那些进行过详尽和深入的汇编语言编程的人那里获取一些“现实生活中”的技巧。

谢谢。

-AD


3
你为什么想要这么做呢?相比使用高级语言,你认为汇编语言编程有哪些优势?而且,这些认为的优势是否真正超过了汇编程序开发和维护的巨大成本? - Paul R
3
在工作中,我将要开发一款SIMD和VLIW处理器以及对应的编译器。但是针对这种架构的公司内部编译器生成的代码效率比手写汇编代码低4到6倍,所以我需要进行改进。 - goldenmean
1
在这种情况下,与其花费大量精力编写汇编语言,如果你将同样的精力投入到改进编译器中,你将获得更高的投资回报率。 - Paul R
假设这是可能的。我猜这是TMS320,唯一的编译器是专有的。 - XTL
@XTL:它们的机器代码给“丑陋”一词赋予了新的含义。 - 6502
投票关闭,原因是过于宽泛。这是我的学习方式:https://github.com/cirosantilli/assembly-cheat/tree/3bb9287e821690f98ddf7c5fda56b456523ba9eb/ia-32 - Ciro Santilli OurBigBook.com
4个回答

21
我的回答一般是......写一个反汇编器。你谈到了ARM,也许你知道所有的ARM指令,也许不知道,那么Thumb呢?ARM是一个很好的学习对象,因为它既流行又固定指令长度,所以你可以从头开始线性地反汇编。
我不是说要写一个完美的源代码级反汇编器,每次只需写5或10行汇编代码,最多也就是相同指令使用不同寄存器,只需用一些if-then-else树或switch语句解析二进制码即可。
add r0,r0,#1
add r0,r1,#1
add r0,r2,#2
你的目标是检查指令中的每个位,理解为什么只能有8位立即数,理解为什么某些处理器只允许您在本地条件分支时跳转127或128个字节。 你不需要写一个反汇编器来做这件事,但对我而言,将信息嵌入我的大脑中非常有效。
为了创建所有可能的操作码/指令以测试反汇编器,你最终会学习到使用您正在使用的汇编程序的所有语法细节。芯片公司书中的汇编语言并不一定是该处理器系列每个汇编器使用的精确语法。mrc/mcr指令(ARM)就是一个很好的例子。特别是gas以更糟糕的方式更改语法,使其比芯片公司的语法和工具更加痛苦。这取决于您想要做什么,如果你只想编写几行代码或修改某些内容,那么你不需要知道每个角落情况或汇编程序的功能,但如果你真的想了解指令集,那么我推荐这种方法。
我也是一名嵌入式软件工程师,主要使用C语言,但每天都会对该C进行反汇编(使用objdump而不是我的工具),检查输出,确保该代码在此内存区域,该代码在此处,链接器等。但有时我必须检查处理器/芯片的模拟,并需要跟随指令提取和它们关联的I/O来跟踪代码通过模拟。或者在RAM或其他总线上使用逻辑分析器调试板子。我学习了许多不同的处理器,8、16、32、64位(还有不在该列表中的寄存器长度)cisc、risc、dsp和几个微型引擎。我为其中的每一个编写了反汇编器(好吧,除了pdp11和x86,我的前两个指令集),一旦你看到了其中几个,学习新的指令集可能只用一个下午。不,对于我来说,从一个我已经日常使用了几天/几周/几个月的处理器切换到我几个月/几年未使用过的处理器需要一两天的时间。我不会同时想着所有语言。拆解可变长度指令(大多数处理器都是如此),真正做到精准无误是一门艺术,远远超出了我所说的范畴,这就是为什么我建议每次只使用少量指令,不要嵌入数据。最好使用此方法时,有一个工作/好用的反汇编器可供参考,以便将输出与实际经过测试和调试的反汇编器进行比较。
如果你真的很热衷于学习,写一个仿真器是一个好练习,我再次强调写而不是检查。许多核心都有仿真器,你可以检查它们而不是编写你自己的,对我有用的未必对你有用。我只写了几个这样的程序。这不是一个下午的项目,但你会更深入地了解该处理器系列的工作原理。
无论哪种学习环境最适合你,无论是反汇编、仿真器、通过基于GUI的ISA模拟器单步执行、书籍、网页等,学习一个或多个处理器的汇编语言肯定会使你的高级编程更好。即使你实际上从未编写汇编程序,只是检查,也是如此。编写一些使用数组、指针、结构体和没有结构体、循环、展开的循环的C代码,使用各种编译器选项进行编译,启用和禁用调试器选项,从不优化到最大/激进优化(为不同的处理器编译并比较程序流程、指令数量等。llvm非常适合这个任务)。
除了使你的高端编码更好之外,你还学会了哪些编译器是好的、坏的和平庸的,应该避免哪些地球语法,哪些语法大多数编译器能正确解析。我强烈推荐尝试尽可能多的不同编译器。
我建议查看截然不同的族系,没有/少量近亲繁殖的族系,我提到了ARM/thumb(和thumb2),它们肯定是近亲繁殖的,但很受欢迎,并且可以支付账单,以便在空闲时间里学习其他族系。回到6802或68hc11,8088和/或z80。老的pic pic12或pic16(pic32只是mips)。mips、power pc、avr。我是msp430指令集的铁杆粉丝,这是一个非常好的学习对象,具有pdp11的感觉,友好的编译器,但可悲的是针对的是一个利基市场。8051仍然未死,令人惊讶。大多数旧的芯片都有各种形式的指令集模拟器(例如mame有许多),所以你可以将这些模拟器打印出来,随着程序的执行观察和学习并改进。然后将这些旧的芯片与更现代的芯片进行比较。查看为什么在相同的时钟频率下,某些ISA跨越了巨大的性能差异,一些具有单个累加器、一个或两个或四个寄存器,要做任何有用的事情,你必须不断地进行加载和存储,需要几个指令才能完成一个实际操作。而一些更现代的处理器通过简单地拥有更多的一个高级话题是内存访问。Thumb(不是Thumb2)比ARM效率低,存在明显的开销,相同任务需要5-10%更多的指令,那么为什么Thumb在GameBoy Advance上速度要快得多?答案是:这主要是因为16位内存总线与非零等待状态内存。GBA没有缓存,但在rom接口上有一个预取处理,并且rom时间是非线性的,第一次读取需要N个时钟周期,随后连续地址读取需要M个时钟周期(其中M小于N)(这使得rom的执行比ram更快)。如果不知道这一点,则对于该平台和其他平台来说,在嵌入式程序中成功或失败可能会有很大差异。这远远超出了汇编语言的理解范围,但您必须能够读取并理解编译器的输出才能达到目标。
另一个高级话题是缓存。如果您可以访问具有缓存功能并可以打开和关闭缓存的设备(例如gamepark gp32或wiz,可以进行homebrew的旧版ipod等),则最好。最理想的情况是您可以单独控制指令和数据缓存。你会感受到一种完全不同的优化方法,它不再仅仅关注最少的指令、最少的跳转/分支和最少的内存访问。现在你必须处理缓存行的长度,指令在缓存行中的位置等问题。在程序开头添加一个、两个、三个甚至更多的nop指令(真的,字面上在start.S中添加一个nop)可以极大地改善或破坏由相同(更高级别)源、编译器和优化设置生成的程序的性能。要查看原因,必须检查指令并理解硬件。
您的特定问题:
-人们在任何汇编语言中编码的经验,他们在多年的汇编语言编码中获得的经验。
请参阅上文。
-学习新的汇编语言时需要注意的准则
请参阅上文。认为处理器更相似而不是不同,它们加载和存储寄存器,无条件和有条件地跳转。同样的一些条件跳转已经广为人知并被使用。首先寻找常见的指令,例如立即加载、将一个寄存器中的值移动到另一个寄存器中,基于寄存器的加、与、或、异或等。并非所有的处理器都有除法指令,大多数没有乘法指令,其中一些没有乘法指令,比你想象的要多。而且,大多数具有乘法指令的处理器是无法通用使用的,如果乘积的操作数和结果都是相同大小寄存器,则许多操作数组合将导致溢出。
-在汇编语言中高效而正确地编写代码的特定提示和技巧
保持简单,不要使用特定于编译器/汇编器或语言的酷技巧。我的20年前的C代码仍然可以在许多编译器上编译。我经常发现一些几年甚至不到的旧代码在今天无法编译,必须通过对新编译器进行维护才能执行相同的功能,仅因为编译器或语言的技巧。
-如何将给定的C代码有效地转换为最佳汇编代码

从C或其他语言开始编译和反汇编,也许要几个级别的优化,也许要几个不同的编译器。然后只需要修复问题即可。这是一个有趣的任务,但你很容易落入那种“哇喔”的陷阱。通常,为了节省5、10或20条指令中的1、2或7条指令而必须使用汇编会导致带着C去搬运汇编代码变得不便携,甚至在下一个版本或两个版本中,编译器就会超越你的能力,因为它们知道更多的指令以及如何使用它们。

我最常使用汇编的地方(除了自然启动)实际上是用于读写寄存器或内存位置。我使用过的每个编译器都曾一度失败了,没有正确生成指令,例如将32位存储器替换为8位存储器等等。我浪费了指令和时钟来实现汇编中的peek和poke例程,以确保编译器不会淹没我。内存复制之类的操作通常非常好(在C库中),但是可以利用指令集。利用特定指令,这些指令不属于您正在使用的语言,例如位测试或位设置(编译器无法识别/优化)。如果存在字节或半字交换指令,则进行字节交换。某些旋转、移位或符号扩展指令。

如果你能找到它,那么就免费使用它,作为黑书的一部分,迈克尔·阿布拉什(Michael Abrash)的《汇编语言禅宗》。测量执行时间并进行测试,测试和测试。不管你认为自己有多么好,秒表都会显示真正的赢家。硬件消除了他教导的一半,但是思考过程以及在那个详细级别上检查代码深度的思考过程(我有原始的印刷版书),后来的杂志文章探讨了超标量处理器以及简单地重新排列一些指令,以便它们可以被识别并分配到单独的执行单元中,使得相同的指令执行速度变快数倍的情况非常有趣并值得一读。这里,大部分内容已经被流水线、更多的执行单元、并行处理、更快的时钟淹没在噪音中。实际上,这都是由于编程语言效率低下,导致硬件必须弥补,但是当我们可以比同行快几千到几万倍地执行相同的操作时,这让我们更加有趣。

但是,使用汇编来改进C代码很容易自掘坟墓,所以要小心谨慎。你已经被警告了。

-如何清晰地理解给定的汇编代码

这就是练习的目的。如果您正在编写自己的汇编程序并驶向道路中央,那么有一些受欢迎、易于阅读、易于编写的指令子集,您很熟悉它们。您可以尝试检查编译器生成的指令,这更难。反汇编器和生成代码一样既是帮助也是问题。手写的老式游戏ROM通常更-如何跟踪寄存器中的操作数,堆栈指针,程序计数器以及更深入了解底层架构和为程序员提供的资源等等。

这通常超出了汇编语言的范畴,您需要了解管道、预取、分支阴影、缓存、写缓冲区、内存总线、等待周期等。
根据你真正想知道的是什么,另一个答案是要了解编译器的调用约定,函数的操作数是否存储在r0、r1、r2 等寄存器中,在它们进入堆栈之前有多少个寄存器。这个编译器是将所有内容都放在堆栈上吗?标志位也存储在堆栈上吗?返回地址存储在哪里?对于同一个目标的不同编译器,例如旧版x86(Zortech/Watcom vs Microsoft/Borland),或者对于同一台处理器但使用不同编译器的现代系统(ABI and EABI),这些都会有所不同。现代时代,你可能会发现某个接口由某人(芯片公司本身?)设计和定义,并且各种编译器出于各种原因(可移植性、营销、懒惰等)都会满足这个标准。我发现检查反汇编并沿着中间道路前进,你可以在不必阅读规格书的情况下找出调用约定。
我早期学习了汇编语言,并且经常会以写汇编程序的方式重复使用泛型变量。因此,在程序中跟踪哪个数据在哪个变量中以及何时处于程序的哪个时刻,对我来说是很自然的。当分析别人的汇编或编译器输出时,我会在使用的文本编辑器中对该输出进行修改。在功能块之间放置可视空格和空行,在每个指令之后添加注释,说明寄存器中现在是什么,例如r0保留表中的索引号,r1现在保留了该项在表中的字偏移量,r0现在保留了该项在表中的物理地址,r2现在保留了来自表中的该项等等。
祝你好运,玩得开心,对于这个非常长的答案,抱歉。

6
一个很好的起点是Jeff Duntemann的书,汇编语言逐步学。该书介绍了Linux下的x86编程。据我回忆,该书的早期版本涵盖了Windows下的编程。这是一本初学者的书,因为它从基础开始讲解:位、字节、二进制算术等。如果你愿意,可以跳过这部分,但至少浏览一下可能是个好主意。
我认为学习ASM编程的最佳方式是:1)学习硬件的基础知识,然后2)研究他人的代码。我提到的那本书是值得一读的。你可能还会对汇编语言程序设计艺术感兴趣。
在我的时间里,我做了相当多的汇编语言编程,尽管在过去的15年中没有做太多。正如一位评论者指出的那样,与高级语言相比,轻微的大小和性能优势难以证明其增加的开发和维护时间的价值。

话虽如此,我不会阻止你提高汇编语言的效率。熟悉处理器在这个级别上的工作只能提高你的高级语言编程技能。


1
这是一个相当广泛的问题,但我可能有一些小贴士。我曾经编写过BAL(IBM 360汇编语言)、8080A/8085、8086/8088/80186、一点点68000(但实际上很少),以及80960汇编语言,还有一点Sparc(尽管是大学课程,所以我不记得具体细节了)。我的专长一直是嵌入式系统,尽管在我的后年里我实际上正在制作网站并且与JavaScript的怪异性斗争。虽然我喜欢我的汇编语言岁月,但现在没什么老年人可以做这个工作了...
我有一些建议给你。首先,你必须学习芯片寄存器;每个寄存器用于什么,它是特殊目的还是通用目的,浮点数运算等。其次,标志位;哪个测试触发哪个标志位,并在这一点上找到设置和清除这些标志位的指令。它们通常很容易,比如CLC('清除进位'-8086/88/186)。找出芯片是大端还是小端,即从左到右的位序是高到低还是低到高。
位操作命令也很重要,例如OR、XOR、通过进位标志移位等。
然后有从内存读写数据的问题;你的芯片汇编语言如何执行这个操作?可以直接写入内存吗?这非常依赖于你的硬件设置。有哪些命令可以将立即值写入寄存器?因为基本上你要做的就是数据的移动:在寄存器和内存之间。内存寻址是平面的(希望)还是分段的(糟糕!)。
如果有时序问题,比如你需要编写代码以充分利用时间,那么你需要设置一个测试框架。记录开始时间、结束时间和中间的代码。我学习汇编语言的主要原因是因为在过去,如果你想要快速,你就得写汇编。
首先,写一些简短的测试版本,然后再开始大量编写。弄清楚如何确定是否已经达到目标(并不是所有东西都有LED指示灯,尽管它们可以让生活更轻松)。我用分析仪。嵌入式系统并不总是可视化的。一开始要小心谨慎。
如果你有编译器,请查看生成适用于你特定芯片的汇编代码。尝试一个小的C函数来相加2个数字,然后去看生成的代码。这样做可以学到很多东西。不要让汇编器指令让你感到不知所措。
我在这里胡言乱语,我想你已经了解了我的意思。从小开始,测试,学习芯片,确保工程师提供给您写入的位置和要写入的内容。如此循环往复...无论如何,祝你好运。我们必须学习,我很喜欢它。我变得非常擅长优化(我喜欢Michael Abrash的Zen of Assembly Language),但Intel芯片的编译器已经被高度优化,因此通常不需要汇编语言程序员。享受吧!

0

最好的方法是编译C代码并查看输出,搜索您不了解的指令的文档。经验会随着时间的推移而来...


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