golang汇编之控制流(五)

news/2024/12/5 11:52:17/

程序执行的流程主要有顺序、分支和循环几种执行流程。本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序,或者说如何以汇编思维来编写Go语言代码。

顺序执行

顺序执行是我们比较熟悉的工作模式,类似俗称流水账编程。所有不含分支、循环和goto语言,并且每一递归调用的Go函数一般都是顺序执行的。

比如有如下顺序执行的代码:

func main() {var a = 10println(a)var b = (a+a)*aprintln(b)
}

我们尝试用Go汇编的思维改写上述函数。因为X86指令中一般只有2个操作数,因此在用汇编改写时要求出现的变量表达式中最多只能有一个运算符。同时对于一些函数调用,也需要改用汇编中可以调用的函数来改写。

第一步改写依然是使用Go语言,只不过是用汇编的思维改写:

func main() {var a, b inta = 10runtime.printint(a)runtime.printnl()b = ab += bb *= aruntime.printint(b)runtime.printnl()
}

首先模仿C语言的处理方式在函数入口处声明全部的局部变量。然后将根据MOV、ADD、MUL等指令的风格,将之前的变量表达式展开为用=+=*=几种运算表达的多个指令。最后用runtime包内部的printint和printnl函数代替之前的println函数输出结果。

经过用汇编的思维改写过后,上述的Go函数虽然看着繁琐了一点,但是还是比较容易理解的。下面我们进一步尝试将改写后的函数继续转译为汇编函数:

TEXT ·main(SB), $24-0MOVQ $0, a-8*2(SP) // a = 0MOVQ $0, b-8*1(SP) // b = 0// 将新的值写入a对应内存MOVQ $10, AX       // AX = 10MOVQ AX, a-8*2(SP) // a = AX// 以a为参数调用函数MOVQ AX, 0(SP)CALL runtime·printintCALL runtime·printnl// 函数调用后, AX/BX 可能被污染, 需要重新加载MOVQ a-8*2(SP), AX // AX = aMOVQ b-8*1(SP), BX // BX = b// 计算b值, 并写入内存MOVQ AX, BX        // BX = AX  // b = aADDQ BX, BX        // BX += BX // b += aMULQ AX, BX        // BX *= AX // b *= aMOVQ BX, b-8*1(SP) // b = BX// 以b为参数调用函数MOVQ BX, 0(SP)CALL runtime·printintCALL runtime·printnlRET

汇编实现main函数的第一步是要计算函数栈帧的大小。因为函数内有a、b两个int类型变量,同时调用的runtime·printint函数参数是一个int类型并且没有返回值,因此main函数的栈帧是3个int类型组成的24个字节的栈内存空间。

在函数的开始处先将变量初始化为0值,其中a-8*2(SP)对应a变量、a-8*1(SP)对应b变量(因为a变量先定义,因此a变量的地址更小)。

然后给a变量分配一个AX寄存器,并且通过AX寄存器将a变量对应的内存设置为10,AX也是10。为了输出a变量,需要将AX寄存器的值放到0(SP)位置,这个位置的变量将在调用runtime·printint函数时作为它的参数被打印。因为我们之前已经将AX的值保存到a变量内存中了,因此在调用函数前并不需要在进行寄存器的备份工作。

在调用函数返回之后,全部的寄存器将被视为被调用的函数修改,因此我们需要从a、b对应的内存中重新恢复寄存器AX和BX。然后参考上面Go语言中b变量的计算方式更新BX对应的值,计算完成后同样将BX的值写入到b对应的内存。

最后以b变量作为参数再次调用runtime·printint函数进行输出工作。所有的寄存器同样可能被污染,不过main马上就返回不在需要使用AX、BX等寄存器,因此就不需要再次恢复寄存器的值了。

重新分析汇编改写后的整个函数会发现里面很多的冗余代码。我们并不需要a、b两个临时变量分配两个内存空间,而且也不需要在每个寄存器变化之后都要写入内存。下面是经过优化的汇编函数:

TEXT ·main(SB), $16-0// var temp int// 将新的值写入a对应内存MOVQ $10, AX        // AX = 10MOVQ AX, temp-8(SP) // temp = AX// 以a为参数调用函数CALL runtime·printintCALL runtime·printnl// 函数调用后, AX 可能被污染, 需要重新加载MOVQ temp-8*1(SP), AX // AX = temp// 计算b值, 不需要写入内存MOVQ AX, BX        // BX = AX  // b = aADDQ BX, BX        // BX += BX // b += aMULQ AX, BX        // BX *= AX // b *= a// ...

首先是将main函数的栈帧大小从24字节减少到16字节。唯一需要保存的是a变量的值,因此在调用runtime·printint函数输出时全部的寄存器都可能被污染,我们无法通过寄存器备份a变量的值,只有在栈内存中的值才是安全的。然后在BX寄存器并不需要保存到内存。其它部分的代码基本保持不变。

if/goto跳转

早期的Go虽然提供了goto语句,但是并不推荐在编程中使用。有一个和cgo类似的原则:如果可以不使用goto语句,那么就不要使用goto语句。Go语言中的goto语句是有严格限制的:它无法跨越代码块,并且在被跨越的代码中不能含有变量定义的语句。虽然Go语言不喜欢goto,但是goto确实每个汇编语言码农的最爱。goto近似等价于汇编语言中的无条件跳转指令JMP,配合if条件goto就组成了有条件跳转指令,而有条件跳转指令正是构建整个汇编代码控制流的基石。

为了便于理解,我们用Go语言构造一个模拟三元表达式的If函数:

func If(ok bool, a, b int) int {if ok { return a } else { return b }
}

比如求两个数最大值的三元表达式(a>b)?a:b用If函数可以这样表达:If(a>b, a, b)。因为语言的限制,用来模拟三元表达式的If函数不支持范型(可以将a、b和返回类型改为空接口,使用会繁琐一些)。

这个函数虽然看似只有简单的一行,但是包含了if分支语句。在改用汇编实现前,我们还是先用汇编的思维来重写If函数。在改写时同样要遵循每个表达式只能有一个运算符的限制,同时if语句的条件部分必须只有一个比较符号组成,if语句的body部分只能是一个goto语句。

用汇编思维改写后的If函数实现如下:

func If(ok int, a, b int) int {if ok == 0 { goto L }return a
L:return b
}

因为汇编语言中没有bool类型,我们改用int类型代替bool类型(真实的汇编是用byte表示bool类型,可以通过MOVBQZX指令加载byte类型的值)。当ok参数非0时返回变量a,否则返回变量b。我们将ok的逻辑反转下:当ok参数为0时,表示返回b,否则返回变量a。在if语句中,当ok参数为0时goto到L标号指定的语句,也就是返回变量b。如果if条件不满足,也就是ok非0,执行后面的语句返回变量a。

上述函数的实现已经非常接近汇编语言,下面是改为汇编实现的代码:

TEXT ·If(SB), NOSPLIT, $0-32MOVQ ok+8*0(FP), CX // okMOVQ a+8*1(FP), AX  // aMOVQ b+8*2(FP), BX  // bCMPQ CX, $0         // test okJZ   L              // if ok == 0, skip 2 lineMOVQ AX, ret+24(FP) // return aRETL:MOVQ BX, ret+24(FP) // return bRET

首先是将三个参数加载到寄存器中,ok参数对应CX寄存器,a、b分别对应AX、BX寄存器。然后使用CMPQ比较指令将CX寄存器和常数0进行比较。如果比较的结果为0,那么下一条JZ为0时跳转指令将跳转到L标号对应的指令,也就是返回变量b的值。如果比较的结果不为0,那么JZ指令讲没有效果,继续执行后的指令,也就是返回变量a的值。

在跳转指令中,跳转的目标一般是通过一个标号表示。不过在有些通过宏实现的函数中,更希望通过相对位置跳转,这时候可以通过PC寄存器来计算跳转的位置。

for循环

Go语言的for循环有多种用法,我们这里只选择最经典的for结构来讨论。经典的for循环由初始化、结束条件、迭代步长三个部分组成,再配合循环体内部的if条件语言,这种for结构可以模拟其它各种循环类型。

基于经典的for循环结构,我们定一个LoopAdd函数,可以用于计算任意等差数列的和:

func LoopAdd(cnt, v0, step int) int {result := v0for i := 0; i < cnt; i++ {result += step}return result
}

比如1+2+...+100可以这样计算LoopAdd(100, 1, 1)10+8+...+0可以这样计算LoopAdd(5, 10, -2)。现在采用前面if/goto类似的技术来改造for循环。

新的LoopAdd函数只有if/goto语句构成:

func LoopAdd(cnt, v0, step int) int {var i = 0var result = 0LOOP_BEGIN:result = v0LOOP_IF:if i < cnt { goto LOOP_BODY }goto LOOP_ENDLOOP_BODYi = i+1result = result + stepgoto LOOP_IFLOOP_END:return result
}

函数的开头先定义两个局部变量便于后续代码使用。然后将for语句的初始化、结束条件、迭代步长三个部分拆分为三个代码段,分别用LOOP_BEGIN、LOOP_IF、LOOP_BODY三个标号表示。其中LOOP_BEGIN循环初始化部分只会执行一次,因此该标号并不会被引用,可以省略。最后LOOP_END语句表示for循环的结束。四个标号分隔出的三个代码段分别对应for循环的初始化语句、循环条件和循环体,其中迭代语句被合并到循环体中了。

下面用汇编语言重新实现LoopAdd函数

// func LoopAdd(cnt, v0, step int) int
TEXT ·LoopAdd(SB), NOSPLIT, $0-32MOVQ cnt+0(FP), AX   // cntMOVQ v0+8(FP), BX    // v0/resultMOVQ step+16(FP), CX // stepLOOP_BEGIN:MOVQ $0, DX          // iLOOP_IF:CMPQ DX, AX          // compare i, cntJL   LOOP_BODY       // if i < cnt: goto LOOP_BODYgoto LOOP_ENDLOOP_BODY:ADDQ $1, DX          // i++ADDQ CX, BX          // result += stepgoto LOOP_IFLOOP_END:MOVQ BX, ret+24(FP)  // return resultRET

其中v0和result变量复用了一个BX寄存器。在LOOP_BEGIN标号对应的指令部分,用MOVQ将DX寄存器初始化为0,DX对应变量i,循环的迭代变量。在LOOP_IF标号对应的指令部分,使用CMPQ指令比较AX和AX,如果循环没有结束则跳转到LOOP_BODY部分,否则跳转到LOOP_END部分结束循环。在LOOP_BODY部分,更新迭代变量并且执行循环体中的累加语句,然后直接跳转到LOOP_IF部分进入下一轮循环条件判断。LOOP_END标号之后就是返回返回累加结果到语句。

循环是最复杂的控制流,循环中隐含了分支和跳转语句。掌握了循环基本也就掌握了汇编语言到写法。掌握规律之后,其实汇编语言编程会变得异常简单。


http://www.ppmy.cn/news/64413.html

相关文章

通过栈/队列/优先级队列/了解容器适配器,仿函数和反向迭代器

文章目录 一.stack二.queue三.deque&#xff08;双端队列&#xff09;四.优先级队列优先级队列中的仿函数手搓优先级队列 五.反向迭代器手搓反向迭代器 vector和list我们称为容器&#xff0c;而stack和queue却被称为容器适配器。 这和它们第二个模板参数有关系&#xff0c;可以…

csgo搬砖项目,时间自由,项目包下车,包落地

Steam是一款全球较大的综合性数字游戏软件发行平台。steam同时在线飙到3300万&#xff01;超越你说熟悉的王者&#xff0c;吃鸡&#xff01;用户多&#xff0c;竞争者少&#xff0c;连我自己都没想到&#xff0c;有一天我居然可以靠着steam游戏搬砖来赚钱养活自己。 实话实说&a…

【分布族谱】Zipf分布及其Python可视化

文章目录 zipf分布简介zipfian和zipf对象zipf分布到zeta分布的变化情况分布族谱图 zipf分布简介 #mermaid-svg-mG901pJXpTYFT7Bk {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-mG901pJXpTYFT7Bk .error-icon{fill:…

一文带你搞清 ChatGPT 与 Azure OpenAI 的区别

这两周是我从2017年开始全职涉入 NLP 领域后最忙的两周&#xff0c;无数的同事和客户都在向我提出一个询问&#xff1a;ChatGPT 可以帮到我们什么&#xff1f; 特别是在2023年3月31日我做了一场微软 Azure OpenAI [布局助力企业]拥抱新智能时代的演讲之后&#xff0c;这几天我…

【大数据之Hadoop】二十七、生产调优-HDFS多目录

1 NameNode多目录配置 NameNode本地目录可以配置多个&#xff0c;每个目录存放内容相同&#xff0c;增加可靠性。 在hdfs-site.xml中添加&#xff0c;每台服务器节点的磁盘不同&#xff0c;可以选择不分发。 <property><name>dfs.namenode.name.dir</name>…

AssetBundle加载与卸载时的内存变化

AssetBundle.LoadFromFile加载一个80MB的assetbundle会分配1MB左右的pss内存 adb分析&#xff1a;private-otherUnityProfiler分析&#xff1a;有3块 1.Other/AssetBundle/LoadingCache 2.Other/SerializedFile/archive:/CAB-e42axxxxxxx 3.NotSaved/AssetBundle/xxxxxx.ab …

QT设置widget属性为FramelessWindowHint导致界面刷新的问题

一.问题描述 当使用继承自QWidget的QT对象时&#xff0c;如果设置了窗口风格&#xff08;FramelessWindowHint&#xff09;为无边框&#xff0c;则在使用 包括 窗口最大化、windows系统&#xff08;winD&#xff09;&#xff0c;图标来回点击显示等操作时&#xff0c;导致界面…

2023年,网络安全方面 5 大值得学习的编程语言

Python 到目前为止&#xff0c;Python 在网络安全领域一直处于领先地位。这是一种通用的服务器端脚本语言&#xff08;无需编译&#xff09;&#xff0c;已经被应用到成千上万的安全项目中。你会发现绝大多数安全工具和 PoCs 都是用 Python 编写的&#xff0c;这样做是有充分理…