我一段时间以来一直想做的事情之一就是真正深入到汇编中,深入了解程序实际是如何运行的。ASM宏的重新编写最近在每晚都会出现,所以现在看起来是个好时机。
与我尝试过的其他一些方法相比,如果我们只是在生锈的操场上做所有的重活,我们需要做的设置要少得多。
我弄清楚事情的过程非常简单,我写了一点生锈的代码,看看汇编输出,然后试着弄清楚发生了什么(通过大量的谷歌搜索)。我将向您介绍我做了什么,以及我弄明白了什么。
您可以通过单击Run旁边的三个点并从下拉菜单中选择ASM来获得此程序集的输出。您可能还想将汇编的风格(通常在其他地方称为语法)更改为英特尔(而不是at&;t)1(如果尚未更改),方法是单击config菜单下的切换。
在调试模式下,这段代码的汇编输出比您预期的要大得多--我得到157行代码。而且大部分都不是我们的节目。不过,我们编写的代码应该很容易找到,因为编译器会很有帮助地用它们的箱名和函数名来标记所有的函数。在本例中,因为我们在操场上,所以create是隐式的操场,所以我们可以通过使用ctrl-f搜索PlayGround::Main来找到我们的代码。这样做可以让我:
因此,即使这是一个调试版本,显然仍然有一些优化正在进行,因为没有数字或任何看起来像是将它们加在一起的东西。这里发生的所有事情就是我们返回(Ret)到调用playround::main的函数。所有以#为前缀的内容都是注释,因此当我们运行此代码时会忽略这些内容。
唯一的另一个兴趣点是标签playround::main:-任何带有:后缀的标签都可以用各种命令跳转到这个标签,实际上,如果我们继续搜索playround::main,我们可以在main中找到对它的一个相当间接的调用。希望在本文结束时我们能理解这一点!
Playround::add:#@playround::add#%bb.0:mov eax,3 ret#--end函数playround::main:#@playround::main#%bb.0:Push rax call playround::add#%bb.1:poprax ret#--end函数
因此,我们在这方面取得了更多进展。仍然在进行一些优化,因为我们在代码中没有看到1或2,只看到了3。我们可以看到被移动(Mov)到了playround::add.中的eax寄存器中。这一定是我们将值返回到main的方式。
事实上,在main内部,我们可以看到将rax-将寄存器rax中的值保存到堆栈,然后调用我们的add函数,然后我们将rax弹出堆栈。推送调用弹出序列将保留ADD中使用的寄存器中的任何值。它还会丢弃我们在add中保存在eax中的值,因为eax和rax是同一个寄存器。下表显示了“较瘦”寄存器与“较宽”寄存器是如何重叠的。
那么我们如何才能让它真正做一些数学运算呢?我们再试一次:
Playround::add:#@playround::add#%bb.0:sub RSP,24 mov qword PTR[RSP+16],RDI add RDI,1 set al test al,1 mov qword PTR[RSP+8],RDI#8字节溢出jne.LBB8_2#%bb.1:MOV rax,qword PTR[RSP+8]#8字节重新加载添加RSP,24 ret.Lb.1:MOV rax,qword PTR[RSP+8]#8字节重新加载添加RSP,24 ret.Lb.1:MOV rax,qword PTR[RSP+8]#8字节重新加载添加RSP,24 ret.Lb.1。Qword PTR[RIP+CORE::FARGING::Panic@GOTPCREL]mov ESI,28调用rax ud2#--end函数playround::main:#@playground::main#%bb.0:Push rax mov edi,2 call playround::add#%bb.1:poprax ret#--end函数。
我们实际上想要生产的东西终于出现在那里了!我们可以看到在输出中添加RDI,1,周围环绕着一堆其他的东西。那么这些其他的代码是什么呢?
让我们主要从调用堆栈的顶部开始。首先,我们可以看到,在调用PlayGround::Add之前,2存储在EDI寄存器中,所以我们知道我们的参数必须在EDI寄存器中。同样,我们可以看到rax上的PUSH、POP,所以这一定是返回值。
现在,看看运动场::ADD,我们首先看到的是SubRSP,24。RSP是保存堆栈指针的寄存器,因此这会增加堆栈(因为堆栈在x862中向下增长)。再往下我们可以看到,使用add rsp,24可以将堆栈缩小相应的量。
然后我们有mov qword ptr[rsp+16],rdi。这是将值从RDI复制到RSP+16处的堆栈上,RSP+16是我们刚刚增长堆栈的区域的顶部。qword PTR(四字(即64位)指针)位是用来消除参数歧义的提示。为什么要把它推到堆栈上呢?我认为这只是为了使调试更容易,因为我们再也不会访问该值了。
在任何情况下,我们都会继续向RDI实际加1。值被存储回RDI中,重要的是,对于接下来要做的事情,我们可能会设置一些标志。
然后事情又变得复杂了-我们有了解决方案。所有的SET*指令都处理标志寄存器。标志寄存器可能是最神奇的寄存器,因为它是由一串指令作为副作用来操作的。
我们运行的最后一条指令是ADD,它设置了6个标志:进位、奇偶校验、调整(也称为辅助进位)、零、符号和溢出。
在本例中,我们检查进位位是否已设置,如果是,则将alregister设置为1。这实际上是在做什么呢?如果我们相加的两个数字有进位,进位位就被设置为1,这意味着得到的数字太大了,无法存储在寄存器中。在这种情况下,我们应该怎么做?让我们继续往下读,找出答案。
然后,在下一行(testal,1)中,我们检查al中的值是否等于1。(test对两个参数执行逐位AND操作-如rust中的&;。)这将设置更多标志,特别是零标志,然后由下面的jne指令读取。
Jne代表JUMP IF NOT EQUAL(同样,还有一系列其他j*指令)。因为它使用标志,所以它只需要一个参数:跳到哪里。
看一下它跳到哪里,我们就会对上面逻辑的意图有一个很大的提示:core::Panching::Panic@GOTPCREL。基本上,从setb到jne的所有汇编块都在检查我们是否溢出了寄存器,如果溢出,就会出现恐慌。
我们没有讨论的一位是mov qword PTR[RSP+8],RDI#8字节溢出。由于注释暗示这是将值从RDI寄存器“溢出”到堆栈上,因为我们可能要跳转到改写该寄存器的代码-在jne之后,我们立即将值从堆栈加载回堆栈。
最后,我们将堆栈指针拖回其起点,并返回调用方。RET使用堆栈上的最后一个值(由调用推送)来确定要跳回的位置,因此将堆栈指针移回非常重要。
因此,也许在这一点上,我们已经看到了足够多的内容,可以尝试用ASM替换addfunction的核心了!宏。因为我们对性能感兴趣,所以我们将忽略那些烦人的溢出检查,并假设我们在U64的范围内。
这里我们要处理的最大的新问题是指定传入和传出寄存器。rfch是对这些寄存器的非常平易近人的解释,所以我建议您阅读它。如果您想自己尝试一下,这里有一个框架,您可以从这里开始。
我设计的版本如下所示。这可能是可能的“最新奇”版本,因为我们使用了尽可能多的ASM宏功能:
我们让RUST编译器挑选我们使用的寄存器,然后使用ASM宏的格式字符串行为编写它。
我们还使用inlateout来提示我们只能使用单个寄存器。
这似乎是一个合理的突破点。我们已经介绍了x64汇编中指令集的合理部分,并查看了大多数指令类的示例。我们可以探索的还有很多,比如:
希望我从这里链接到的资源足以让你继续挖掘,如果你想要的话,也许我会设法跟进这一点。
这是我发现关于组装最令人困惑的事情之一。(至少)有两种不同的主要语法,它们只是在语法上不同。因此指令是相同的(add、mov等),但是它们以不同的顺序接受它们的参数。AT&;T风格的组件中也有一堆随机符号。由于Rusts ASM默认采用英特尔风格的组装,我们将坚持这样做。如果你真的开始在谷歌上搜索东西,你会得到什么样的组装,这是掷骰子决定的。美国电话电报公司(AT&;T)有很多%符号,所以这通常是免费的。
[返回]