一些汇编小技巧

写一些我自己不知道的汇编小技巧,持续更新。

汇编预处理器指令(The NASM Preprocessor)

本节主要以NASM指令和相关实例为主。如果想要了解更多NASM预处理器指令可以参考这篇文章 或者 NASM手册

rep指令

rep 指令可以将代码块重复若干次:

1
2
3
4
%rep 5         
dec ecx
jnz .next
%endrep

展开版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dec ecx
jnz .next

dec ecx
jnz .next

dec ecx
jnz .next

dec ecx
jnz .next

dec ecx
jnz .next

assign指令

assign指令用于向一个变量赋值,比如 %assign i 100,也可以将变量的值赋给它,比如 %assign i i+1

实例

上面两个宏指令看上去很普通,但是我曾经在写benchmark的时候写过这种超级冗余的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mov r8,100000
ALIGN 32
.begin:
.loop_1:
dec ecx
jge .loop_2
.loop_2:
dec ecx
jge .loop_3
.loop_3:
dec ecx
jge .loop_4
.loop_4:
.loop_5:
dec ecx
jge .loop_6
;...
;...
.loop_8000:
dec r8
jge .begin

有了 repassign,就可以把它变得很简洁了,从20007行减少到了12行…

当然前面那个也不是手写的,是程序生成的。但是下面这种写法还是方便很多

1
2
3
4
5
6
7
8
9
10
11
12
13
mov r8,100000
ALIGN 32
.begin:
%assign i 1
%rep 7999
.loop_%+i
dec ecx
%assign i i+1
jge .loop_%+i
%endrep
.loop_8000:
dec r8
jge .begin

align 指令

align指令主要用于对齐,可以加快汇编程序的运行速度。通常是添加 nop 达到对齐的目的。需要注意的是align指令后的数字必须是2的幂。

假定以下代码段从地址0(或者4的倍数)开始:

1
2
3
4
add eax,1
add ecx,2
Align 4
add edx,3

那么它的等价表达就是

1
2
3
4
5
6
.text:
add eax,0x1 ;83 c0 01
add ecx,0x2 ;83 c1 02
nop ;90
nop ;90
add edx,0x3 ;83 c3 03

其中 align 指令还可以使用 0 而不是 nop 作为填充,比如 align 4,db 0,它对应的机器码是 0000 ( 对应的汇编指令是 add BYTE PTR [rax],al )。

alignb 指令可以用于数据的对齐,作为性能优化的一种手段。

%comment

可以使用%comment%endcomment 来注释多行。

1
2
3
4
5
%comment
add eax,1
add ecx,2
add edx,3
%endcomment

如何用shell脚本编译汇编程序

只涉及编译汇编程序部分,更详细的论述请参考runoob教程。1

使用shell脚本编译汇编程序的目的是为了多次测试汇编程序或者修改某些关键变量的值。

shell脚本中可以常规的进行赋值,也可以使用语句给变量赋值。在使用的时候只要加上 $ 即可。

1
2
3
file = "test.asm"
for file in `ls /etc`
for file in $(ls /etc)

为了控制宏变量的值,我们需要使用 for 循环。以下两种格式都可以:

1
2
3
4
5
6
7
8
9
for var in item1 item2 ... itemN
do
command1
command2
...
commandN
done

for var in item1 item2 ... itemN; do command1; command2… done;

在nasm中使用如下格式定义宏变量: -Dmacro[=str]

所以一个完整的shell脚本如下:

1
2
3
4
5
6
7
8
9
10
for NOPS in $(seq 0 20) 
do

nasm -f elf64 test.asm -g -DNOPS=$NOPS -o test.o && ld -m elf_x86_64 test.o -o test

echo $(expr $NOPS);

perf -d ./test

done

而在汇编程序内,需要有这样的指令,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/ixor r, r/i 的latency都是1周期,而reciprocal throughput都为0.25(可以并发处理4条独立的相同指令)。

但是如果考虑到CPU的乱序执行的话,xor eax,eax 使用了 eax 作为输入,那么是否存在数据依赖的风险从而导致指令被序列化执行呢?8

考虑如下代码块:

1
2
3
4
add eax,1
mov ebx,eax
xor eax,eax
add eax,ecx

我们希望寄存器能够知道 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
2
3
4
5
6
mov    eax,-1          ;b8 ff ff ff ff

xor eax,eax ;31 c0
dec eax ;ff c8

or eax,-1 ;83 c8 ff

初始化 eax 为 全1

根据peter cordes的答案,可以列出如下选项10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mov eax, -1         ;b8 ff ff ff ff

mov rax, -1 ;48 c7 c0 ff ff ff ff

xor eax, eax ;31 c0
dec rax ;48 ff c8

xor ecx, ecx ;31 c9
lea eax, [rcx-1] ;8d 41 ff

or eax, -1 ;83 c8 ff

push -1 ;6a ff
pop rax ;58

从代码长度上来看,后两个无疑是最佳选择,但是牺牲了一部分性能。由于peter没有写benchmark,只是做了一些定性说明,所以我决定对性能做一个详细的测试。

TL ; DR 选mov的两组。

To be continued.


参考

Author

moep0

Posted on

2021-11-29

Updated on

2021-11-29

Licensed under

Comments