我的回答一般是......写一个反汇编器。你谈到了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现在保留了来自表中的该项等等。
祝你好运,玩得开心,对于这个非常长的答案,抱歉。