我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Golang汇编之控制流深入分析讲解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Golang汇编之控制流深入分析讲解

顺序执行

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

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

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

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

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

func main() {
	var a, b int
	a = 10
	runtime.printint(a)
	runtime.printnl()
	b = a
	b += b
	b *= a
	runtime.printint(b)
	runtime.printnl()
}

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

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

TEXT ·main(SB), $24-0
    MOVQ $0, a-8*2(SP) // a = 0
    MOVQ $0, b-8*1(SP) // b = 0

    // 将新的值写入a对应内存
    MOVQ $10, AX       // AX = 10
    MOVQ AX, a-8*2(SP) // a = AX

    // 以a为参数调用函数
    MOVQ AX, 0(SP)
    CALL runtime·printint
    CALL runtime·printnl

    // 函数调用后, AX/BX 可能被污染, 需要重新加载
    MOVQ a-8*2(SP), AX // AX = a
    MOVQ b-8*1(SP), BX // BX = b

    // 计算b值, 并写入内存
    MOVQ AX, BX        // BX = AX  // b = a
    ADDQ BX, BX        // BX += BX // b += a
    MULQ AX, BX        // BX *= AX // b *= a
    MOVQ BX, b-8*1(SP) // b = BX

    // 以b为参数调用函数
    MOVQ BX, 0(SP)
    CALL runtime·printint
    CALL runtime·printnl

    RET

汇编实现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 = 10
    MOVQ AX, temp-8(SP) // temp = AX

    // 以a为参数调用函数
    CALL runtime·printint
    CALL runtime·printnl

    // 函数调用后, AX 可能被污染, 需要重新加载
    MOVQ temp-8*1(SP), AX // AX = temp

    // 计算b值, 不需要写入内存
    MOVQ AX, BX        // BX = AX  // b = a
    ADDQ BX, BX        // BX += BX // b += a
    MULQ 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-32
    MOVQ ok+8*0(FP), CX // ok
    MOVQ a+8*1(FP), AX  // a
    MOVQ b+8*2(FP), BX  // b

    CMPQ CX, $0         // test ok
    JZ   L              // if ok == 0, skip 2 line
    MOVQ AX, ret+24(FP) // return a
    RET

L:
    MOVQ BX, ret+24(FP) // return b
    RET

首先是将三个参数加载到寄存器中,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 := v0
	for 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 = 0
	var result = 0
LOOP_BEGIN:
	result = v0
LOOP_IF:
	if i < cnt { goto LOOP_BODY }
	goto LOOP_END
LOOP_BODY
	i = i+1
	result = result + step
	goto LOOP_IF
LOOP_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-32
	MOVQ cnt+0(FP), AX   // cnt
	MOVQ v0+8(FP), BX    // v0/result
	MOVQ step+16(FP), CX // step
LOOP_BEGIN:
	MOVQ $0, DX          // i
LOOP_IF:
	CMPQ DX, AX          // compare i, cnt
	JL   LOOP_BODY       // if i < cnt: goto LOOP_BODY
	goto LOOP_END
LOOP_BODY:
	ADDQ $1, DX          // i++
	ADDQ CX, BX          // result += step
	goto LOOP_IF
LOOP_END:
	MOVQ BX, ret+24(FP)  // return result
	RET

其中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标号之后就是返回返回累加结果到语句。

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

到此这篇关于Golang汇编之控制流深入分析讲解的文章就介绍到这了,更多相关Go语言汇编之控制流内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Golang汇编之控制流深入分析讲解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Golang汇编之控制流深入分析讲解

这篇文章主要介绍了Golang汇编之控制流,程序执行的流程主要有顺序、分支和循环几种执行流程,本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序,或者说如何以汇编思维来编写Go语言代码,感兴趣的同学可以参考下文
2023-05-20

MySQL流程控制函数汇总分析讲解

目录1.IF函数2.IFNULL函数3.CASE函数4.多重IF在 mysql 中,流程控制函数是指可以控制存储过程(stored procedure)或函数(function)中执行流程的语句。以下是几个常用的流程控制函数:1.IF函数
2023-04-24

深入理解Golang流程控制语句

go 语言提供了丰富的流程控制语句,用于控制程序流程流向,包括:条件语句(if、switch);循环语句(for、while);实战案例:计算阶乘使用 if 和 for 语句;其他流程控制语句(break、continue、goto、def
深入理解Golang流程控制语句
2024-04-04

Android权限机制深入分析讲解

Android的权限管理遵循的是“最小特权原则”,即所有的Android应用程序都被赋予了最小权限。一个Android应用程序如果没有声明任何权限,就没有任何特权
2022-12-08

AndroidView的事件分发机制深入分析讲解

事件分发从手指触摸屏幕开始,即产生了触摸信息,被底层系统捕获后会传递给Android的输入系统服务IMS,通过Binder把消息发送到activity,activity会通过phoneWindow、DecorView最终发送给ViewGroup。这里就直接分析ViewGroup的事件分发
2023-01-29

必学!深入解析Python中常用的流程控制语句

小白必看!Python中常用的流程控制语句解析,需要具体代码示例导语:Python作为一门简洁而强大的编程语言,具有简单易学的特点,适合初学者入门。而流程控制语句是编程中的核心,通过掌握流程控制语句,可以让你的程序编写更加灵活和高效。本文
必学!深入解析Python中常用的流程控制语句
2024-01-20

深度解析Python流程控制语句:有多少种分类?

Python作为一种高级编程语言,以其简洁明了和易读性强而受到广大开发者的青睐。在Python中,流程控制语句是编写程序时必不可少的重要部分。本文将带您深入了解Python中流程控制语句的种类及其具体代码示例,帮助您更好地掌握Python编
深度解析Python流程控制语句:有多少种分类?
2024-01-20

深入解析Python流程控制语句:if、else、elif、while、for的使用

Python流程控制语句详解:if、else、elif、while、for在编程中,流程控制语句是必不可少的,它们用于根据条件决定程序的执行流程。Python提供了几个常用的流程控制语句,包括if、else、elif、while和for。
深入解析Python流程控制语句:if、else、elif、while、for的使用
2024-01-20

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录