- 1 -
有一天,有一个程序员叫做 Bob,他写了一个用 Go 语言实现的阶乘函数。但是,当数据规模变大时,运行速度非常慢。于是,他的老板让他改成用汇编语言实现。Bob 很不情愿,但还是学习了汇编语言,并写出了一个能跑得飞快的阶乘函数。
最后,Bob 成为了一名出色的汇编程序员,并因为他的阶乘函数而获得了巨大的成功。他明白,汇编语言可以让他的程序更快、更简洁。
所以,如果你想写出高效、简洁的程序,就要学习汇编语言。
- 2 -
咱们先从简单的入手,用 Go 实现一个完整的阶乘函数:
package main
import "fmt"
func factorial(n int) int {
result := 1
for i := 1; i <= n; i++ {
result *= i
}
return result
}
func main() {
fmt.Println(factorial(5)) // Output: 120
}
- 3 -
接下来,我们把 factorial 函数转换成汇编语言。
首先,我们要在 Go 代码里将 factorial 函数的函数体移除,只包含函数定义(声明),这告诉 Go 编译器,该函数会在另一个文件里由汇编实现。
package main
import "fmt"
func factorial(n int) int
func main() {
fmt.Println(factorial(5))
}
然后,我们新建一个文件 fac.s,内容如下:
TEXT ·factorial(SB), $0-8
MOVQ n+0(FP), CX
MOVQ $1, DX
LOOP:
IMULQ CX, DX
DECQ CX
JNZ LOOP
MOVQ DX, result+8(FP)
RET
编译运行:
$ go run .
120
- 4 -
这段代码是一个用汇编语言实现的阶乘函数。它的作用是计算给定数字 n 的阶乘。
它的实现方式是使用一个无限循环来计算阶乘。在循环的每一次迭代中,它将结果与当前的数字相乘,然后将当前的数字减 1。最后,它将结果存储到 result 变量中,并退出函数。
接下来咱们逐句分析这段代码:
TEXT ·factorial(SB), $0-8
TEXT 表示这个方法在 TEXT 段中
· 是Unicode的「中点」(中文输入法,1左边的按键),前面省略了包名,表示这是 main 包的 factorial 函数
SB 是 stack base pointer,Go ASM 中的「伪寄存器」(不是硬件寄存器),大致等同于程序的起始地址
$0-8:0 表示这个函数没有局部变量,8 表示返回值占用8个字节
MOVQ n+0(FP), CX
MOVQ 的 Q 表示 8 个字节
n+0(FP) 表示变量 n 在 FP(Frame Pointer,伪寄存器,表示这个函数的栈帧起始位置) + 0 的位置(即第一个参数)。注意 Go ASM 要求形式上必须是「变量名+偏移量(FP)」这个写法,但是变量名n没有实际意义,只是用来助记。
CX 即 x86/x86_64 的 CX(16bit),ECX(32bit),RCX(64bit) 寄存器,具体多长取决于前面的指令(MOVQ是64bit)
这句的意思是把第一个参数的值写入 RCX
MOVQ $1, DX
$1:$开头的是立即数
这句的意思是给 RDX 赋值为 1
IMULQ CX, DX
DX = DX * CX
DECQ CX
CX = CX - 1
JNZ LOOP
JNZ: Jump if Not Zero
当CX 不等于 0 时跳转到 LOOP
MOVQ DX, result+8(FP)
将 RDX 的值写入到 FP+8 的位置。
RET
返回到调用方。
- 5 -
需要注意的是,为了实现上更简洁,这段汇编代码和Go代码并不是等价的。
如果输入的 n 为负数,会导致代码出错。这是因为,这段代码中没有判断边界条件,所以如果输入的 n 为负数,就会无限循环下去,造成程序运行时间过长或者程序崩溃。
- 6 -
最后,这篇文章是 ChatGPT 和我一起完成的。
细心如你,应该发现了哪些是它的贡献,以及它犯的一个错误。
p.s. 题图由 6pen.art 生成,关键词「Golang 汇编语言 阶乘」。
参考:
[1] Golang ASM 简明教程:https://jiajunhuang.com/articles/2020_04_22-go_asm.md.html
[2] A Quick Guide to Go's Assembler:https://go.dev/doc/asm