Answers:
mov
-立即数对于常量来说很昂贵这可能很明显,但我仍将其放在此处。通常,在需要初始化值时,考虑数字的位级表示会很有意义。
eax
有0
:b8 00 00 00 00 mov $0x0,%eax
应该缩短(出于性能以及代码大小的考虑)以
31 c0 xor %eax,%eax
eax
有-1
:b8 ff ff ff ff mov $-1,%eax
可以缩短为
31 c0 xor %eax,%eax
48 dec %eax
要么
83 c8 ff or $-1,%eax
或更一般而言,可以在3个字节push -12
(2个字节)/ pop %eax
(1个字节)中创建任何8位符号扩展值。 这甚至适用于没有额外REX前缀的64位寄存器。push
/ pop
默认操作数大小= 64。
6a f3 pushq $0xfffffffffffffff3
5d pop %rbp
或在寄存器中给定已知常量,则可以使用lea 123(%eax), %ecx
(3个字节)创建另一个附近的常量。如果您需要一个清零的寄存器和一个常量,这将很方便。异或零(2个字节)+ lea-disp8
(3个字节)。
31 c0 xor %eax,%eax
8d 48 0c lea 0xc(%eax),%ecx
dec
xor eax, eax; dec eax
push imm8
/ pop reg
是3个字节,对于x86-64上的64位常量非常有用,其中dec
/ inc
是2个字节。和push r64
/ pop 64
(2个字节)甚至可以替换一个3字节mov r64, r64
(3个字节与REX)。另请参见设置在CPU寄存器中的所有位为1有效的一样的东西lea eax, [rcx-1]
在给定一个已知值eax
(例如,如果需要的归零寄存器和另一个不变,只是使用LEA,而不是推/弹出
在很多情况下,基于累加器的指令(即,那些(R|E)AX
作为目标操作数的指令)比一般情况下的指令短1个字节。在StackOverflow上看到这个问题。
al, imm8
特殊的情况下,像or al, 0x20
/ sub al, 'a'
/ cmp al, 'z'-'a'
/ ja .non_alphabetic
为各2个字节,而不是3.使用al
字符数据还允许lodsb
和/或stosb
。或使用al
测试一些关于EAX的低字节,像lodsd
/ test al, 1
/ setnz cl
使CL = 1或0用于奇数/偶数。但是在极少数情况下,您需要立即使用32位,然后确定op eax, imm32
,就像我的色键回答
答案的语言是asm(实际上是机器代码),因此应将其视为以asm编写的程序的一部分,而不是C-compiled-x86。 您的函数不必通过任何标准的调用约定就可以轻松地从C调用。但是,如果它不花费您任何额外的字节,那将是一个不错的奖励。
在纯asm程序中,某些帮助程序函数使用对他们和他们的调用者都方便的调用约定是正常的。这些函数用注释记录了它们的调用约定(输入/输出/修饰符)。
在现实生活中,即使是asm程序(我认为)也倾向于对大多数功能(尤其是在不同的源文件中)使用一致的调用约定,但是任何给定的重要功能都可以执行某些特殊操作。在代码高尔夫球中,您正在优化单个功能中的废话,因此显然这很重要/特别。
要从C程序测试功能,可以编写一个包装器,将args放在正确的位置,保存/恢复您破坏的所有额外寄存器,并将返回值放入(e/rax
如果还不存在的话)。
要求将DF(lods
/ stos
/等的字符串方向标志)清除(向上)在调用/重拨中是正常的。可以让它在通话/重拨时不确定。要求将其清除或在输入时进行设置,但是在返回时将其保留为修改状态将很奇怪。
在x87中返回FP值st0
是合理的,但st3
在其他x87寄存器中不带垃圾返回是不合理的。调用者必须清理x87堆栈。即使st0
使用非空的较高堆栈寄存器返回也将是有问题的(除非您要返回多个值)。
call
,因此[rsp]
您的返回地址也是如此。您可以在x86上使用/ 这样的链接寄存器来避免call
/ ret
并使用返回,但这并不是“合理的”。这没有调用/重调用那么有效,因此您可能无法在真实代码中找到它。lea rbx, [ret_addr]
jmp function
jmp rbx
临界情况:写一个在数组中产生序列的函数,给定前两个元素作为函数args。 我选择让调用者将序列的开始存储到数组中,然后仅将指针传递给数组。这肯定是在弯曲问题的要求。我考虑将参量打包为xmm0
for movlps [rdi], xmm0
,这也是一个怪异的调用约定。
OS X系统调用执行此操作(CF=0
表示没有错误):将标志寄存器用作布尔值返回值是否被认为是不好的做法?。
可以使用一个JCC进行检查的任何条件都是完全合理的,尤其是如果您可以选择与该问题有语义相关性的条件。(例如,比较函数可能会设置标志,因此jne
如果它们不相等,则将被采用)。
char
)作为符号或零,以扩展为32或64位。这不是不合理的。在现代x86 asm中,使用movzx
或movsx
避免部分寄存器变慢是很正常的。实际上,clang / LLVM已经制作了依赖于x86-64 System V调用约定的未公开文档扩展的代码:小于32位的args由调用者符号化或为0扩展为32位。
如果需要,您可以通过编写uint64_t
或int64_t
在原型中记录/描述64位扩展。例如,因此您可以使用一条loop
指令,该指令使用RCX的全部64位,除非您使用地址大小前缀将大小覆盖为32位ECX(是的,实际上,地址大小不是操作数大小)。
注意,long
在Windows 64位ABI和Linux x32 ABI中,这只是32位类型。uint64_t
是明确的,比短unsigned long long
。
Windows 32位__fastcall
,已经由另一个答案建议:ecx
and中的整数args edx
。
x86-64系统V:在寄存器中传递大量的args,并且具有很多调用中断的寄存器,您可以在不使用REX前缀的情况下使用它们。更重要的是,实际上是选择它来允许编译器轻松地内联memcpy
或memset rep movsb
:前6个整数/指针args在RDI,RSI,RDX,RCX,R8,R9中传递。
如果您的函数在运行时间的循环内(使用指令)使用lodsd
/ ,则可以说“ 与x86-64 System V调用约定一样,可以从C 调用”。 例如:chromakey。stosd
rcx
loop
int foo(int *rdi, const int *rsi, int dummy, uint64_t len)
32位GCC regparm
:EAX,ECX,EDX中的整数args,以EAX(或EDX:EAX)返回。将第一个arg与返回值放在同一寄存器中可以进行一些优化,例如这种情况的示例调用者和带有function属性的原型。当然,AL / EAX对于某些说明也很特殊。
Linux x32 ABI在长模式下使用32位指针,因此您可以在修改指针时保存REX前缀(示例用例)。您仍然可以使用64位地址大小,除非您在寄存器中有一个零扩展的32位负整数(因此,如果这样做的话,它将是一个很大的无符号值[rdi + rdx]
)。
请注意,push rsp
/ pop rax
是2个字节,等效于mov rax,rsp
,因此您仍然可以2个字节复制完整的64位寄存器。
ret 16
; 弹出args 。他们不会弹出返回地址,先推送一个数组,然后是push rcx
/ ret
。调用者必须知道数组的大小,或者将RSP保存在堆栈外部的某个地方才能找到自己。
对AL / AX / EAX使用特殊情况的短格式编码,以及其他短格式和单字节指令
示例假定为32/64位模式,其中默认操作数大小为32位。操作数大小的前缀将指令更改为AX而不是EAX(或在16位模式下为相反)。
inc/dec
寄存器(8位除外):inc eax
/ dec ebp
。(不是x86-64:0x4x
操作码字节被重新用作REX前缀,所以inc r/m32
也是唯一的编码。)
8位inc bl
是2个字节,使用inc r/m8
操作码+ ModR / M操作数编码。因此,使用inc ebx
以增量bl
,如果它是安全的。(例如,如果在高字节可能不为零的情况下不需要ZF结果)。
scasd
:e/rdi+=4
,要求寄存器指向可读存储器。即使您不在乎FLAGS结果,有时也很有用(例如cmp eax,[rdi]
/ rdi+=4
)。和在64位模式下,scasb
可以作为1字节inc rdi
,如果LODSB或STOSB是没有用的。
xchg eax, r32
:这是0x90 NOP的来源:xchg eax,eax
。示例:在8个字节的GCDxchg
的cdq
/ idiv
循环中,用两个指令为3个寄存器重新排列3个寄存器,其中大多数指令为单字节,包括滥用inc ecx
/ loop
代替test ecx,ecx
/jnz
cdq
:将EAX符号扩展到EDX:EAX,即将EAX的高位复制到EDX的所有位。用已知的非负数创建零,或用0 / -1进行加/减或掩码。 x86历史课程:cltq
vsmovslq
。,还有AT&T与Intel助记符有关cdqe
。
lodsb / d:喜欢mov eax, [rsi]
/ rsi += 4
没有破坏标志。(假设DF是清楚的,则函数输入需要哪些标准调用约定。)也是stosb / d,有时是scas,很少是movs / cmps。
push
/ pop reg
。例如在64位模式下,push rsp
/ pop rdi
是2个字节,但mov rdi, rsp
需要一个REX前缀,并且是3个字节。
xlatb
存在,但很少有用。避免使用较大的查找表。我也从未发现可用于AAA / DAA或其他打包的BCD或2 ASCII数字指令。
1字节lahf
/ sahf
很少有用。您可以 lahf
/ and ah, 1
替代setc ah
,但通常没有用。
特别是对于CF,sbb eax,eax
将获得0 / -1,甚至是未记录但普遍支持的1字节salc
(由Carry设置为AL),在sbb al,al
不影响标志的情况下有效地起作用。(在x86-64中删除)。我在用户赞赏挑战#1:Dennis♦中使用了SALC 。
1字节cmc
/ clc
/ stc
(翻转(“补体”),清除或设置CF)是很少有用,虽然我发现了使用cmc
在扩展精度除了用碱10 ^ 9块。为了无条件设置/清除CF,通常将其安排为另一条指令的一部分,例如xor eax,eax
清除CF和EAX。对于其他条件标志,只有DF(字符串方向)和IF(中断)没有等效的指令。进位标志对于许多指令来说都是特殊的。移位设置它,adc al, 0
可以将其添加到2个字节的AL中,我之前提到了未记录的SALC。
std
/ cld
很少值得。尤其是在32位代码中,最好仅对ALU指令使用dec
指针和a mov
或内存源操作数,而不用将DF设置为so 向下lodsb
/ stosb
而不是向上。通常,如果你在所有的向下需要,你还有另一个指针往上走,所以你需要不止一个std
,并cld
在全功能使用lods
/ stos
两个。相反,仅使用向上的字符串指令即可。(标准调用约定保证函数输入上的DF = 0,因此您可以免费假设不使用cld
。)
在最初的8086,AX是非常特殊:说明喜欢lodsb
/ stosb
,cbw
,mul
/ div
和其他隐含的使用它。当然还是这样。当前的x86尚未删除8086的任何操作码(至少不是任何正式记录的操作码)。但是后来的CPU添加了新的指令,这些指令提供了更好/更有效的方法来执行操作,而无需先将它们复制或交换到AX。(或以32位模式转到EAX。)
例如8086缺少后面的加法,例如movsx
/ movzx
来加载或移动+符号扩展,或2和3操作数imul cx, bx, 1234
,它们不会产生上半部分的结果并且没有任何隐式操作数。
而且,8086的主要瓶颈是指令提取,因此优化代码大小对于当时的性能至关重要。8086 的ISA设计师(Stephen Morse)在AX / AL的特殊情况下花了很多操作码编码空间,包括特殊的(E)AX / AL目的地操作码,用于所有基本的Instant-src ALU-指令,只是opcode + Instant没有ModR / M字节。2字节add/sub/and/or/xor/cmp/test/... AL,imm8
或AX,imm16
或(在32位模式下)EAX,imm32
。
但是,没有特殊情况EAX,imm8
,因此的常规ModR / M编码add eax,4
更短。
假设是,如果您要处理某些数据,则需要在AX / AL中进行操作,因此与AX交换寄存器是您可能想要做的事情,甚至可能比将寄存器复制到AX时更经常mov
。
大约8086指令编码的所有内容都支持该范例,从类似指令lodsb/w
到EAX立即数的所有特殊情况编码到隐式使用(甚至用于乘法/除法)。
不要被带走;将所有内容交换到EAX并非自动取胜,特别是如果您需要使用32位寄存器而不是8位寄存器的立即数。或者,如果您需要一次交错操作寄存器中的多个变量。或者,如果您使用带有2个寄存器的指令,则根本不使用立即数。
但请始终牢记:我在做EAX / AL短一些的事情吗?我可以重新排列一下,以便在AL中使用它吗?或者我目前在将AL用于它的用途上是否更好地利用了AL。
只要安全就可以自由地混合使用8位和32位运算,以利用它(您不需要将其转入完整寄存器或其他任何东西)。
cdq
在许多情况下对于div
需要清零edx
的情况很有用。
fastcall
约定x86平台有许多调用约定。您应该使用那些在寄存器中传递参数的参数。在x86_64上,无论如何前几个参数都会传递到寄存器中,所以在那里没有问题。在32位平台上,默认的调用约定(cdecl
)在堆栈中传递参数,这对打高尔夫球不利-在堆栈上访问参数需要很长的指令。
fastcall
在32位平台上使用时,通常在ecx
和中传递2个第一个参数edx
。如果函数具有3个参数,则可以考虑在64位平台上实现它。
fastcall
约定的C函数原型(从此示例答案中获取):
extern int __fastcall SwapParity(int value); // MSVC
extern int __attribute__((fastcall)) SwapParity(int value); // GNU
0100 81C38000 ADD BX,0080
0104 83EB80 SUB BX,-80
同样,加-128而不是减128
< 128
为<= 127
减少对立即数的大小cmp
,或GCC 总是倾向于重新安排进行比较以减小幅度,即使它不是-129与-128。
mul
(然后inc
/ dec
以获得+1 / -1以及零)您可以通过在第三个寄存器中乘以零来使eax和edx归零。
xor ebx, ebx ; 2B ebx = 0
mul ebx ; 2B eax=edx = 0
inc ebx ; 1B ebx=1
将导致EAX,EDX和EBX在四个字节中都为零。您可以将EAX和EDX归零为三个字节:
xor eax, eax
cdq
但是从那开始,您将无法再在一个字节中获得第三个零寄存器,或者在另外2个字节中获得+1或-1寄存器。而是使用mul技术。
用例示例:将Fibonacci数字连接为binary。
请注意,LOOP
循环结束后,ECX将为零,并可用于将EDX和EAX归零。您不必总是使用创建第一个零xor
。
我们可以假定CPU处于基于平台和OS的已知且已记录的默认状态。
例如:
DOS http://www.fysnet.net/yourhelp.htm
Linux x86 ELF http://asm.sourceforge.net/articles/startup.html
_start
。因此,是的,如果您要编写程序而不是函数,则可以利用它。我是在Extreme Fibonacci中这样做的。(在动态链接的可执行文件中,ld.so在跳转到您的之前运行_start
,并且确实在寄存器中留下了垃圾,但是静态只是您的代码。)
要加减1,请使用一个字节inc
或dec
比多字节加法和减法子指令小的指令。
inc/dec r32
,其寄存器号已在操作码中编码。所以inc ebx
是1个字节,但是inc bl
是2个。仍然小于add bl, 1
当然对于比其他寄存器al
。另请注意,inc
/ dec
保留CF不变,但更新其他标志。
lea
数学这可能是了解x86的第一件事,但在此提醒我。lea
可用于乘以2、3、4、5、8或9,然后加上一个偏移量。
例如,要ebx = 9*eax + 3
在一条指令中进行计算(在32位模式下):
8d 5c c0 03 lea 0x3(%eax,%eax,8),%ebx
这里没有偏移量:
8d 1c c0 lea (%eax,%eax,8),%ebx
哇!当然,lea
也可以用来做数学运算,例如ebx = edx + 8*eax + 3
计算数组索引。
lea eax, [rcx + 13]
是64位模式的无前缀版本。32位操作数大小(用于结果)和64位地址大小(用于输入)。
mov
适用时将小立即数放入较低的寄存器如果您已经知道寄存器的高位为0,则可以使用较短的指令将立即数移入低位寄存器。
b8 0a 00 00 00 mov $0xa,%eax
与
b0 0a mov $0xa,%al
push
/pop
表示imm8高零位归功于Peter Cordes。xor
/ mov
是4个字节,但是push
/ pop
只有3 个字节!
6a 0a push $0xa
58 pop %eax
mov al, 0xa
如果您不需要将其零扩展到完整的reg,那就很好。但是,如果这样做,则xor / mov为4字节,而推送imm8 / pop或lea
其他已知常量为3字节。如果与mul
零个3个4个字节的寄存器或组合使用,这可能很有用cdq
,但是如果您需要很多常量。
xchg eax, r32
),例如mov bl, 10
/ dec bl
/ jnz
所以你的代码不关心高字节RBX的。
经过许多算术指令后,进位标志(无符号)和溢出标志(有符号)会自动设置(更多信息)。经过许多算术和逻辑运算后,才设置符号标志和零标志。这可以用于条件分支。
例:
d1 f8 sar %eax
ZF由该指令设置,因此我们可以将其用于常规分支。
test al,1
; 您通常不会免费获得。(或and al,1
根据奇/偶创建一个整数0/1。)
test
/ cmp
”,那么那将是非常基本的初学者x86,但仍然值得赞扬。
这不是x86专用的,而是广泛适用的初学者组装技巧。如果您知道while循环将至少运行一次,请将循环重写为do-while循环,并在最后进行循环条件检查,通常会保存2字节的跳转指令。在特殊情况下,您甚至可以使用loop
。
do{}while()
组装中自然循环成语(特别是为了提高效率)。还要注意,2字节jecxz
/ jrcxz
循环前可以很好loop
地处理“需要零次运行”的情况(在loop
不慢的罕见CPU上)“有效地”处理。 jecxz
也可使用内部循环来实现while(ecx){}
,以jmp
在底部。
System V的x86使用堆栈和System V X86-64用途rdi
,rsi
,rdx
,rcx
,等输入参数,并rax
作为返回值,但它是完全合理的使用自己的调用约定。__fastcall使用ecx
和edx
作为输入参数,其他编译器/ OS使用它们自己的约定。方便时,使用堆栈和任何寄存器作为输入/输出。
示例:重复字节计数器,对1字节解决方案使用了巧妙的调用约定。
其他资源:Agner Fog关于调用约定的注释
int 0x80
需要大量设置。
int 0x80
用32位或syscall
64位代码来调用sys_write
是唯一的好方法。这就是我用于Extreme Fibonacci的方式。在64位代码中,__NR_write = 1 = STDOUT_FILENO
您可以这样做mov eax, edi
。或者,如果EAX的高位字节为零,则为mov al, 4
32位代码。您也可以,call printf
或者puts
,我想写一个“用于Linux + glibc的x86 asm”答案。我认为不计算PLT或GOT入口空间或库代码本身是合理的。
char*buf
并以手动格式生成字符串。例如像这样(笨拙地优化了速度)asm FizzBuzz,我在其中将字符串数据存入寄存器,然后用进行存储mov
,因为字符串较短且长度固定。
CMOVcc
和集合SETcc
这更让我想起了问题,但是在处理器P6(Pentium Pro)或更高版本上存在条件设置指令,并且存在条件移动指令。有许多指令基于EFLAGS中设置的一个或多个标志。
cmov
具有2字节的操作码(0F 4x +ModR/M
),因此最少3字节。但是源是r / m32,因此您可以有条件地加载3个字节。除了分支以外,setcc
在更多情况下还有用cmovcc
。尽管如此,请考虑整个指令集,而不仅仅是基线386指令。(尽管SSE2和BMI / BMI2指令是如此之大,以至于它们很少有用。它 rorx eax, ecx, 32
是6字节,比mov + ror长。对于性能而言,这是不错的选择,除非POPCNT或PDEP节省了许多isns,否则对高尔夫来说不是很好)
setcc
。
jmp
通过安排if / then而不是if / then / else 节省字节这当然是非常基本的,只是想我会把它发布为打高尔夫球时要考虑的事情。例如,请考虑以下简单代码来解码十六进制数字字符:
cmp $'A', %al
jae .Lletter
sub $'0', %al
jmp .Lprocess
.Lletter:
sub $('A'-10), %al
.Lprocess:
movzbl %al, %eax
...
通过使“ then”情况变为“ else”情况,可以将其缩短两个字节:
cmp $'A', %al
jb .digit
sub $('A'-'0'-10), %eax
.digit:
sub $'0', %eax
movzbl %al, %eax
...
sub
一种情况下,关键路径上的额外延迟不是循环携带的依赖链的一部分时(例如,此处每个输入数字在合并4位块之前都是独立的) )。但是我还是想+1。顺便说一句,您的示例有一个单独的错过的优化:如果仍然需要movzx
在末尾使用a ,则sub $imm, %al
不要使用EAX来利用的无现代2字节编码op $imm, %al
。
cmp
通过执行操作消除sub $'A'-10, %al
;jae .was_alpha
; add $('A'-10)-'0'
。(我认为我的逻辑正确)。请注意,'A'-10 > '9'
因此没有歧义。减去字母的更正将包装一个十进制数字。因此,如果像您一样假定输入是有效的十六进制,这是安全的。
您可以通过将esi设置为esp并执行一系列lodsd / xchg reg eax来从堆栈中获取顺序对象。
pop eax
/ pop edx
/ ...?如果需要将它们留在堆栈上,则可以push
在恢复ESP之后将它们全部恢复,每个对象仍然需要2个字节,而无需mov esi,esp
。还是对64位代码中的4字节对象意味着pop
8字节?顺便说一句,您甚至可以使用pop
比性能更好的缓冲区循环lodsd
,例如在Extreme Fibonacci中扩展精度添加
要复制64位寄存器,请使用push rcx
;pop rdx
而不是3个字节mov
。
push / pop的默认操作数大小为64位,而无需REX前缀。
51 push rcx
5a pop rdx
vs.
48 89 ca mov rdx,rcx
(操作数大小的前缀可以将push / pop操作数大小改写为16位,但是即使在REX.W = 0的情况下,在64位模式下也无法编码32位push / pop操作数大小。)
如果一个或两个寄存器都是r8
.. r15
,请使用,mov
因为push和/或pop将需要REX前缀。最糟糕的情况是,如果两个都需要REX前缀,则实际上会丢失。显然,在代码高尔夫中,您通常应该避免使用r8..r15。
使用此NASM宏进行开发时,可以使源代码更具可读性。只需记住,它在RSP下面的8个字节上移动。(在x86-64 System V中的红色区域中)。但在正常情况下,它可以直接替代64位mov r64,r64
或mov r64, -128..127
; mov %1, %2 ; use this macro to copy 64-bit registers in 2 bytes (no REX prefix)
%macro MOVE 2
push %2
pop %1
%endmacro
例子:
MOVE rax, rsi ; 2 bytes (push + pop)
MOVE rbp, rdx ; 2 bytes (push + pop)
mov ecx, edi ; 2 bytes. 32-bit operand size doesn't need REX prefixes
MOVE r8, r10 ; 4 bytes, don't use
mov r8, r10 ; 3 bytes, REX prefix has W=1 and the bits for reg and r/m being high
xchg eax, edi ; 1 byte (special xchg-with-accumulator opcodes)
xchg rax, rdi ; 2 bytes (REX.W + that)
xchg ecx, edx ; 2 bytes (normal xchg + modrm)
xchg rcx, rdx ; 3 bytes (normal REX + xchg + modrm)
该xchg
示例的一部分是因为有时您需要获取EAX或RAX的值,而不必关心保留旧副本。但是,push / pop并不能帮助您进行实际交流。
push 200; pop edx
-3个字节进行初始化。