用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) 执行下一条命令

gdb调试更多可看

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* 12

int 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

可以看出,用条件跳转模式会多计算一些指令,不会破坏流水线,如果在流水线上减少的时间大于多进行指令所消耗的时间,那么用条件跳转指令比较合适。反之,直接用条件指令。

下列几种情况不适合用条件跳转指令

  1. 分支语句计算量大(执行时间过多,划不来)
  2. 在要判断是否可以运算的情况,例如 var=p ?*p : 0,这条语句意思是如果p不为空指针那么将p的内容赋给var,否则赋0.但是如果用条件跳转会出错,因为空指针不能取值
  3. 计算可能产生副作用(即前后两个分支之间会产生影响)。例如 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 逻辑右移