一些汇编小技巧
写一些我自己不知道的汇编小技巧,持续更新。
汇编预处理器指令(The NASM Preprocessor)
本节主要以NASM指令和相关实例为主。如果想要了解更多NASM预处理器指令可以参考这篇文章 或者 NASM手册。
rep指令
rep
指令可以将代码块重复若干次:
1 | 5 |
展开版本如下:
1 | dec ecx |
assign指令
assign指令用于向一个变量赋值,比如 %assign i 100
,也可以将变量的值赋给它,比如 %assign i i+1
。
实例
上面两个宏指令看上去很普通,但是我曾经在写benchmark的时候写过这种超级冗余的代码
1 | mov r8,100000 |
有了 rep
和 assign
,就可以把它变得很简洁了,从20007行减少到了12行…
当然前面那个也不是手写的,是程序生成的。但是下面这种写法还是方便很多
1 | mov r8,100000 |
align 指令
align
指令主要用于对齐,可以加快汇编程序的运行速度。通常是添加 nop
达到对齐的目的。需要注意的是align指令后的数字必须是2的幂。
假定以下代码段从地址0(或者4的倍数)开始:
1 | add eax,1 |
那么它的等价表达就是
1 | .text: |
其中 align
指令还可以使用 0 而不是 nop
作为填充,比如 align 4,db 0
,它对应的机器码是 0000
( 对应的汇编指令是 add BYTE PTR [rax],al
)。
alignb
指令可以用于数据的对齐,作为性能优化的一种手段。
%comment
可以使用%comment
和 %endcomment
来注释多行。
1 |
|
如何用shell脚本编译汇编程序
只涉及编译汇编程序部分,更详细的论述请参考runoob教程。1
使用shell脚本编译汇编程序的目的是为了多次测试汇编程序或者修改某些关键变量的值。
shell脚本中可以常规的进行赋值,也可以使用语句给变量赋值。在使用的时候只要加上 $
即可。
1 | file = "test.asm" |
为了控制宏变量的值,我们需要使用 for
循环。以下两种格式都可以:
1 | for var in item1 item2 ... itemN |
在nasm中使用如下格式定义宏变量: -Dmacro[=str]
所以一个完整的shell脚本如下:
1 | for NOPS in $(seq 0 20) |
而在汇编程序内,需要有这样的指令,nop
就会被重复 NOPS
次了。
1 | times NOPS nop |
不知道该用在哪
首先介绍下 Code golf,指的是用尽可能短的源代码实现某种算法。
所以在实现时会出现很多奇怪的技巧(几乎都是用于减少汇编代码长度的)3,也许可以用在嵌入式里面。
初始化eax
初始化 eax 为0
当需要初始化 eax
为 0 的时候,不应该使用:
1 | mov eax,0x0 ;b8 00 00 00 00 |
而应该使用
1 | xor eax,eax ;31 c0 |
这节省了3 byte。当然还有其他的资源和能耗上的考量。6 7
查阅agner的手册可知,在Icelake上,mov r, r/i
和 xor r, r/i
的latency都是1周期,而reciprocal throughput都为0.25(可以并发处理4条独立的相同指令)。
但是如果考虑到CPU的乱序执行的话,xor eax,eax
使用了 eax
作为输入,那么是否存在数据依赖的风险从而导致指令被序列化执行呢?8
考虑如下代码块:
1 | add eax,1 |
我们希望寄存器能够知道 xor
指令的结果并不依赖于 add
指令的结果。
从网上的存档来看,CPU可以做到的(由于找不到这么老的架构,我没有办法复现)。并且在sandybridge之后,intel引入了 zeroing idioms,许多常见的归零习语(zeroing idioms)被识别,并且寄存器被简单地设置为零。这些优化的完成速度与重命名期间的重命名速度相同(每个周期 4 µOP)。9 也就是说,xor eax, eax
甚至是不占用port资源的。
初始化 eax 为 1
当需要初始化 eax
为 1 的时候,也可以有类似的操作。需要注意的是 dec eax
在64位中的机器码为 ff c8
,而在32位中的机器码为 48
。
所以后两个表达在32位中是等价的,但是在64位中后面一个要节省一个byte。
1 | mov eax,-1 ;b8 ff ff ff ff |
初始化 eax 为 全1
根据peter cordes的答案,可以列出如下选项10
1 | mov eax, -1 ;b8 ff ff ff ff |
从代码长度上来看,后两个无疑是最佳选择,但是牺牲了一部分性能。由于peter没有写benchmark,只是做了一些定性说明,所以我决定对性能做一个详细的测试。
TL ; DR 选
mov
的两组。
To be continued.
参考
- 1.Shell 教程 ↩
- 2.NASM手册阅读笔记(8) - 其他 ↩
- 3.Tips for golfing in x86/x64 machine code ↩
- 4.What does .p2align do in asm code? ↩
- 5.x86 Assembly Language Reference Manual ↩
- 6.Set all bits in CPU register to 1 efficiently ↩
- 7.What is the best way to set a register to zero in x86 assembly: xor, mov or and? ↩
- 8.The Surprising Subtleties of Zeroing a Register ↩
- 9.skylake zeroing idioms ↩
- 10.Set all bits in CPU register to 1 efficiently ↩