汇编指令 AT&T版 64位
用g++ -Og -S -masm=intel name.c 可以生成intel格式的汇编代码,-S是生成汇编代码,-Og是省去不重要的部分,如果不加这个,可能5条指令要变成十多条指令
-fno-if-conversion 分支语句不要采用条件传输的方式,条件跳转的方式
-g debug
objdump -d 反汇编
gdb 调试 b+ 标号 设置断点 info register 查看寄存器 r 执行到断点出
n(next) 执行下一条命令
mov 移动指令
格式: movq a,b
a是原操作数,b是目标操作数
这个指令中的q代表的是64位
长度 | 类型 | 别名 | 缩写 |
---|---|---|---|
1 | char | byte | b |
2 | short | word | w |
4 | int | double word(long word) | l |
8 | long | quad word | q |
8 | char* | quad word | q |
4 | float | … | s |
8 | double | … | l |
源操作数 | 目标操作数 |
---|---|
立即数(idata) | 寄存器 ,地址 |
寄存器 | 寄存器,地址 |
地址 | 寄存器 |
操作数 | 符号 |
---|---|
立即数 | $ |
寄存器 | % |
地址 | () |
地址中数据 | 不加任何符号 |
例:movq $2,%rax
movq %rax,%rbx
movq (%rax),%rbx
注意;
- 当源操作数为地址的时候,目标操作数不能也是地址,因为一定要通过cpu进行调控,从地址到地址代表没有经过cpu(解释是这样,但我觉得不太合理)
- 立即数不能为8字节,使用 movabsq 可以让立即数为8字节
- 地址都是8字节,也就是上面的char*
- 如果把一个32位数给64位寄存器,那么高三十二位自动变成0.但是如果是16位或者8位的话高位不会改变
例如:
movabsq $0x0011223344556677,%rax
movq $-1,%rax
%rax= 00000000ffffffff
地址的写法
地址可以写为一个四元组 A(寄存器1,寄存器2,B)
这个四元组其实是 寄存器1+B*寄存器2+A
其中B可以是1,2,4,8。从数组的角度来看,寄存器1相当于起始地址,寄存器2相当于偏移地址,B是一个数据的字节数
同时 64位寄存器前缀是r,三十二位前缀是e,16位没有前缀,8位后面是l和h
例:rax
eax
ax
al
ah
强制类型转换
格式 movzab
a代表原格式,b代表现格式,一般是从小的转换成大的。而z代表多的位补0
例: movzlq
特殊形式: cltq 把32位寄存器扩展到64位并且赋值给自己,
movsab 有符号扩展,最高位为1那么扩展出来的位全是1。
leaq 地址转移
格式 leaq 地址,寄存器
看起来与mov没什么区别,但是mov中的地址是要取地址中的数,而这里的地址只是把对应地址给寄存器
例如: leaq (%rax,%rbx,4),%rdx 这条指令类似于把数组中某一元素的地址给了rdx
它还可用来求值
例如 要求x* 12int mul(int x)
{
leaq (%rdi,%rdi,2),%rax
salq %rax,2//左移指令
return x;
}
这段代码只要看中间两条指令就可以了,leaq 中的数运算之后是3rdi,然后左移两位就是12了。
但是这条指令不会判断溢出,且不会改变标志寄存器
比较指令
这里涉及到标志寄存器,在前面一篇博客中已经提到
cmpq 比较指令
textq 与比较
textq b a 把a与b相与,不改变a和b的值,只会改变标志寄存器中的zf和sf。
这个指令通常用来判断某一位是否是1,例如 textq a,0x1 ,如果最低位为1,那么zf就是0,说明最低位是1
标志寄存器的访问
操作名 | 含义 | 描述 |
---|---|---|
sete | ZF | Equal |
setne | ~ZF | not equal |
sets | SF | sign |
setns | ~SF | |
setg | ~(SF^OF)&~ZF | greater 有符号数 |
setge | ~(SF^OF) | 大于等于 |
setl | (SF^OF) | less |
setle | … | 小于等于 |
seta | ~CF&~ZF | above 无符号数 |
setb | CF | below 小于 |
这些指令都是返回到8位寄存器上,将高七位置0,最低位依据含义所进行的运算来判断是0还是1
之后可以 movzbl %al,%eax,这时高三十二位也会被清零
跳转指令
这里再补充jg和gb等,其实就是上面同样的模式
此外,由于是64位系统,没有cs寄存器,且intel使用cisc指令集,所以会自动根据指令长度进行判断用多少位长来储存位移
此外 x86 64位系统中 还提供了一种 jmp *(%rax) 也就是把rax作为地址,取内存中的内容
条件跳转指令
格式: cmov +后缀 a b,后缀格式就是前面set的格式
作用: 如果满足条件,则把a赋值给b,如果不满足,则不做处理
现代编译器在遇到分支语句时会尽量用条件跳转的格式执行,因为cpu采用了流水线作业的模式(就是一次提前搬运多条指令) 但是条件跳转指令会使流水线停止运行,而流水线可以明显增加cpu速度。
条件跳转的格式就是先把 if 和else的内容都计算出来,然后在最后用条件跳转指令进行比较。
例如int a;
if(x>y)
{
a=x-y;
}
else
{
a=y-x;
}
汇编模式
movq %rdi,%rax
subq %rsi,%rax
movq %rsi,%rdx
subq %rdi,%rdx
cmovle %rdx,%rax
可以看出,用条件跳转模式会多计算一些指令,不会破坏流水线,如果在流水线上减少的时间大于多进行指令所消耗的时间,那么用条件跳转指令比较合适。反之,直接用条件指令。
下列几种情况不适合用条件跳转指令
- 分支语句计算量大(执行时间过多,划不来)
- 在要判断是否可以运算的情况,例如 var=p ?*p : 0,这条语句意思是如果p不为空指针那么将p的内容赋给var,否则赋0.但是如果用条件跳转会出错,因为空指针不能取值
- 计算可能产生副作用(即前后两个分支之间会产生影响)。例如 var=x>0 ? x*=3 : x+=6,这个分支前面对后面会产生影响,因此不能简单粗暴的直接条件跳转
循环语句
- do-while循环
do-while循环用goto语句表达成loop:
statement;
if(x) goto loop;
这段代码便是先执行,后比较。很容易就可以转化成汇编。goto可以用条件转移指令代替。
- while循环
while循环有两种形式,第一种是在最开始进行一次跳转,如果符合则进入循环,不符合则退出if(!x) goto done;
loop:
statement;
if(x) goto loop;
done:
第二种是先跳过第一次循环直接进行判断。goto test;
loop:
statement;
test:
if(x) goto loop;
由于cpu流水线的限制,两条连续的跳转指令会减慢cpu运行的速度,因此最好采用第一种办法(虽然代码多)
- for循环
for(init; test; update)
可以转化成init;
while(test)
{
statement;
update;
}
再然后就可以变成汇编代码
switch 汇编实现
首先要了解条件语句和switch的区别,if语句要从上倒下一条一条判断,如果数量多的话时间开销大。而switch是根据标号直接跳转,无论要跳转到哪一个时间开销都一定。
内存中实际上在编译时已经设定了一个跳转表,这个跳转表的标号是一个固定的地址,不能被改变,这个标号内的数据就是每一条指令的跳转地址,而且是8字节。
因此跳转语句可以这样写: jmp .L4(,%rdi,8)
其中.L4代表的是跳转表的标号(不一定是这个名字,举个例子)而rdi就是switch中的x,
如果标号很大,例如10000甚至1000000开始的时候,如果把前面的查找表一个个全部设置出来,空间开销会很大,所以编译器会先自动的减去一个数使他处于较小的范围,但是这种情况只适用于数据密集的情况。
如果数据稀疏例如一个是1一个是10000,这个时候偏移也不行了,只有通过先排序再二分搜索来查找标号,这个复杂度是logn
杂项
- inc 自增
- dec 自减
- neg 取负 -x
- not 取非 ~x
- imul 乘
- xor 异或
- or 或
- and 与
- sal shl 左移
- sar 算数右移
- shr 逻辑右移