文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

什么是PeachPy

PeachPy是一个基于Python的框架,用于在程序中编写模块。 它自动化了一些细节,并允许你使用Python生成重复的汇编代码序列。

PeachPy支持写模块,您可以直接通过Go for x86-64使用。 (它还支持NaCl和syso模块,但我不会在这篇文章中讲那些。)

这篇文章将主要是讲你需要知道的有关集成PeachPy的事情和一些PeachPy相关的教程。

此帖子的所有代码都在github.com/dgryski/peachpy-examples

第 1 段(可获 1.2 积分)

一个简单的例子

让我们从一个简单的函数开始:取两个整数并返回它们的和。

import peachpy.x86_64

f1 = Argument(uint64_t)
f2 = Argument(uint16_t)

with Function("add", (f1, f2), uint64_t) as function:
    reg_f1 = GeneralPurposeRegister64()
    reg_f2 = GeneralPurposeRegister64()

    LOAD.ARGUMENT(reg_f1, f1)
    LOAD.ARGUMENT(reg_f2, f2)

    ADD(reg_f1, reg_f2)

    RETURN(reg_f1)

接下来,我们将使用go:generate指令添加一个函数桩:

// +build amd64

package main

//go:generate python -m peachpy.x86_64 add.py -S -o add_amd64.s -mabi=goasm
func add(a uint64, b uint16) uint64
第 2 段(可获 0.35 积分)

当我们运行go generate时, 我们得到以下汇编代码:

// Generated by PeachPy 0.2.0 from add.py


// func add(f1 uint64, f2 uint16) uint64
TEXT ·add(SB),4,$0-24
	MOVQ f1+0(FP), AX
	MOVWQZX f2+8(FP), BX
	ADDQ BX, AX
	MOVQ AX, ret+16(FP)
	RET

让我们逐块回顾下PeachPy的代码。

f1 = Argument(uint64_t)
f2 = Argument(uint16_t)

首先,我们声明两个参数。我们在这里声明的类型用于确定实参传递到函数的位置和方式,对于自动生成的函数原型。Go函数原型仅在生成的程序集文件中的注释中。这里没有能力将这些参数与实际的函数桩匹配,因此不匹配这里不会检测到。

第 3 段(可获 1.06 积分)
with Function("add", (f1, f2), uint64_t) as function:

该行声明了一个函数 add 带有两个参数(f1, f2) 并带有一个uint64_t类型的返回值。

    reg_f1 = GeneralPurposeRegister64()
    reg_f2 = GeneralPurposeRegister64()

接下来,我们获得两个寄存器。在这种情况下,我们不关心实际的寄存器是什么。PeachPy将为我们分配两个。

    LOAD.ARGUMENT(reg_f1, f1)
    LOAD.ARGUMENT(reg_f2, f2)

这些都是PeachPy伪指令它会将参数加载到所选寄存器中。这样可以隐藏细节允许便携式组装。使用GO调用约定参数是传递到堆栈上, 所以PeachPy生成适当的 MOV 指令。 如果用PeachPy为常规的C调用约定生成代码,这些将从适当的寄存器加载参数。

第 4 段(可获 1.24 积分)
    ADD(reg_f1, reg_f2)

    RETURN(reg_f1)

最后一个块执行添加任务,然后设置返回值。 这里再一次出现的RETURN是一个PeachPy伪指令,它将为Go的调用生成适当的结果存储到栈中。

还要注意,操作数是Intel语法顺序(目标优先),而不是Plan9 / AT&T顺序(源在前)。 PeachPy根据目标生成正确的汇编语法。

然后我们可以将其称为常规Go函数。

func main() {
	sum := add(100, 20)
	fmt.Printf("sum = %+v\n", sum)
}

使用结构体

第 5 段(可获 0.94 积分)

这个例子将讨论通过引用一个函数来传递一个结构体。

首先,为了能够访问任何字段,您需要知道它们相对于struct的基址的偏移量。 unsafe.Offsetof 函数将为您执行此操作,但Dominik Honnef的structlayout工具也是如此。

例如,给定

type foo struct {
	zot uint16
	bar uint16
	qux uint64
}

我们可以查询所有字段的偏移量:

    $ structlayout github.com/dgryski/peachpy-examples/struct foo
    foo.zot uint16: 0-2 (size 2, align 2)
    foo.bar uint16: 2-4 (size 2, align 2)
    padding: 4-8 (size 4, align 0)
    foo.qux uint64: 8-16 (size 8, align 8)
第 6 段(可获 0.84 积分)

这是我们的peachpy代码:

import peachpy.x86_64

f = Argument(ptr())

with Function("add", (f,), uint64_t) as function:
    reg_f_base = GeneralPurposeRegister64()

    LOAD.ARGUMENT(reg_f_base, f)

    v = GeneralPurposeRegister64()

    # move and zero-extend 16-bit value at addr reg_f_base+2
    MOVZX(v, word[reg_f_base+2])
    ADD(v, [reg_f_base+8])

    RETURN(v)

这一次我们有一些区别。 首先,参数类型被声明为ptr()。 这相当于一个C的void *, Go的uintptr。

第二,当声明函数本身时,只有一个参数,因为PeachPy期望一个元组作为第二个参数,我们必须写成(f,)。

第 7 段(可获 0.74 积分)

函数的主体开始是相同的。 我们将struct(第一个参数)的地址加载到reg_f_base中。

接下来,我们声明一个临时寄存器v来存储总和。

我们接下来的两个指令加载bar的值(在偏移量2处的字大小的值)并且添加qux(偏移量8)

最后,我们设置v作为函数的返回值。

//go:generate python -m peachpy.x86_64 add.py -S -o add_amd64.s -mabi=goasm
//go:noescape
func add(f *foo) uint64

我们的存根文件在函数声明之前包括一个附加的伪指令。 // go:noescape指令告诉编译器,传递给此函数的指针不会转储到堆或返回值中。 没有这个,编译器将别无选择,只能在堆上分配结构。 现在,编译器的转义分析可以告诉它可能被分配在堆栈上(因为它将作为我们的主函数)。 这可能是一个重大的胜利。 另一方面,堆栈分配可以中断某些SSE指令所需的对齐,需要使用未对齐的版本。

第 8 段(可获 2.01 积分)

这是我们的main函数:

func main() {
	var f = foo{bar: 200, qux: 50000}
	sum := add(&f)
	fmt.Printf("sum = %+v\n", sum)
}

运行它,我们会得到预期的结果:

sum = 50200

使用切片

有两种方式处理切片。

你可以传递第一个元素的地址并使用它,然后你的纯Go版本和你的asm版本将有不同的参数列表。 这更贴近C调用您的代码,并使您为C和Go生成汇编更加容易。

另一方面,由于你不能在Go中做指针运算,你的pure-Go版本(你正在编写编程序的纯Go版本),仍然需要传递一个切片。 这意味着你会有代码看起来像

第 9 段(可获 1.5 积分)
var total uint64
if useGo {
   total = addGo(x)
} else {
   total = addAsm(&x[0], len(x))
}

但是如果你只是写Go代码,如果他们是一样的这样更好。 (你仍然可以从C调用你的slice-expecting-assembly例程 - 你只需要两次传递长度来假装成容器)。

让我们看一个例子。 这里是函数的参数:

s_base = Argument(ptr())
s_len = Argument(size_t)
s_cap = Argument(size_t)

切片作为三个参数传递:指向数据的指针,长度和容量。 从技术上讲,len和cap是有符号的int。 但是,我们将使用size_t,尽管它是无符号的。 (这里的signedness只用于生成生成的程序集的原型注释,并且不影响指令。此外,使用size_t在语义上比使用ptrdiff_t更好,ptrdiff_t是一个自适应大小的有符号整数,PeachPy只使用 生成堆栈偏移时的整数的大小。如果你想要更精确,你只能在amd64上运行,你可以使用精确的int64_t来代替。

第 10 段(可获 2.01 积分)

这里我们的函数声明提到了我们期望的三个参数。 我们还可以看到使用PeachPy的Loop()函数来构造切片的所有元素。

with Function("add", (s_base,s_len,s_cap), uint64_t) as function:
    reg_s_base = GeneralPurposeRegister64()
    reg_s_len = GeneralPurposeRegister64()

    LOAD.ARGUMENT(reg_s_base, s_base)
    LOAD.ARGUMENT(reg_s_len, s_len)

    total = GeneralPurposeRegister64()
    XOR(total, total)

    with Loop() as loop:
        ADD(total, [reg_s_base])
        ADD(reg_s_base, 8)
        SUB(reg_s_len, 1)
        JNZ(loop.begin)

    RETURN(total)
第 11 段(可获 0.38 积分)

虽然我们的集合stub只列出一个切片:

func add(s []uint64) uint64

go vet工具将检查切片的装配参数是否命名正确。 如果你有一个参数s是一个slice,你的汇编函数的三个参数应该是s_base,s_len和s_cap。

使用字符串

字符串只是一个没有容量的slice,所以你只需要声明两个参数:数据指针和长度。

调试

Delve支持单步执行汇编代码,这是很好的。 它不支持查看任何扩展寄存器

第 12 段(可获 1.11 积分)

由于PeachPy支持标准C语言调用,因此也可以使用C语言调用汇编程序,并可以使用标准Linux调试器调试它,而不必担心Go运行时的细节。

例如,我们使用上面的切片代码可以生成一个sysv elf对象文件

python -m peachpy.x86_64 ../add.py -emit-c-header add.h -mimage-format=elf -o add_amd64.o -mabi=sysv

然后使用C语言调用它

int main() {
    uint64_t t[]={100, 200, 1000, 50000};
    printf("sum=%ld\n", add(&t, 4, 4));
}

测试

测试汇编代码与测试常规代码没有什么不同。 然而,建议你做一个纯Go版本的程序,这个程序也去做你的汇编程序正在做的事情。 这可以在平台上使用,也可以作为基本版本来比较您的优化实施。 如果你在构建时使用构建标签选择一个实现,你需要在模糊化时重命名函数,这样你可以在你的测试二进制文件中拥有这两个函数。

第 13 段(可获 1.65 积分)

输入可以来自您的常规测试套件,或通过go-fuzz开发。 有关使用go-fuzz用于对照实施的更多信息,请参阅DNS解析器,遇见Go fuzzer

最后,您还可以利用PeachPy在Python中进行测试。 这将需要通过Python的ctypes模块构造参数列表。

一个简单的例子来测试我们的代码片断:

if __name__ == "__main__":
    import ctypes
    add_asm = function.finalize(abi.detect()).encode().load()
    inp = [10,500,2000,50000]
    arr = (ctypes.c_ulonglong * len(inp))(*inp)
    g = add_asm(arr,len(arr),len(arr))
    assert(g == 52510)
第 14 段(可获 0.76 积分)

每次PeachPy生成汇编代码时都可以在你的python代码中进行测试。

资源

Go汇编器的官方文档在https://golang.org/doc/asm。 它们对读取很有用,但请记住,PeachPy将会处理关于语法和调用约定的许多细节。

PeachPy源

最后,在2016年GolangUK上,Michael Munday做了关于向下扩展:Go的汇编功能的演讲。

第 15 段(可获 0.99 积分)

文章评论