外联汇编
64位下的CPU由于新增了很多寄存器,所以不开启任何优化级别的G++都开始使用寄存器传递函数参数了,比如 printf() 函数使用的就是寄存器传递参数
所以如果一定要以外联汇编的形式优化程序的,那么估计又要记住参数传递使用寄存器规则索性使用内联汇编的形式优化,更加自然
内联汇编
G++中的内联汇编分为基本形式的内联汇编与扩展形式的内联汇编;毫无疑问,扩展形式的内联汇编更加复杂,也更加强大
__asm__与asm
两者是一样的,只不过ANSI C标准将asm作为关键字用于其他用途;所以为了与ANSIC兼容,还是使用__asm__;
__volatile__于volatile
告诉编译器,此处禁止优化,与__asm__一同使用,表示不优化内联汇编段
基本形式的内联汇编
可以使用宏定义,来更加方便得使用内联汇编,如
#define _mBeginASM __asm__ __volatile__ (#define _mEndASM );
基本形式的内联汇编语法如:
_mBeginASM"汇编代码"_mEndASM
G++会将"汇编代码"部分逐字插入到为程序生成的汇编代码中,所以应该在每一条指令后面手动添加'\n\t';如
#include#define _mBeginASM __asm__ __volatile__ (#define _mEndASM );int a=44;int b=33;int c;int main(int argc,char *argv[]){ _mBeginASM "movl a,%eax\n\t" "addl b,%eax\n\t" "movl %eax,c" _mEndASM printf("%d\n",c);/* 运行可以看出c为77 */ return 0;}/* 使用 g++ -S -o Hello.s Hello.cpp 查看编译器生成的汇编文件*//* 全局变量就是直接定义在 .data 段中的数据,而且编译器对变量名并没有使用名称修饰 */ .data .align 4 .type a, @object .size a, 4a: .long 44 .globl b .align 4 .type b, @object .size b, 4b: .long 33 .globl c .bss .align 4 .type c, @object .size c, 4c: .zero 4/* 生成的内联汇编 */#APP# 15 "Hello.cpp" 1 movl a,%eax addl b,%eax movl %eax,c# 0 "" 2#NO_APP/* 为了验证G++是将"汇编代码"逐字插入到为程序生成的汇编代码中,可以试着去掉每一条汇编指令中的'\n\t'; */
扩展形式的内联汇编
语法形式:
_mBeginASM"汇编代码模板" :输出参数列表:输入参数列表:改动的寄存器列表_mEndASM
总的作用机理就是:
g++ 首先根据'输入参数列表'部分将输入参数复制到指定的寄存器(使用mov,fld..等指令);
替换一下汇编代码模板中的占位符,然后将该部分逐字插入到为程序生成的汇编代码中
根据'输出参数列表'部分将指定寄存器中的值mov到C++变量中
感觉就像一个函数调用,'汇编代码模板'就是函数体,'输入参数列表'部分指定了函数的参数,'输出参数列表'部分指定了函数的返回值
汇编代码模板
与基本形式的内联汇编相似,除了多了占位符(%0,%1,...表示着输入参数输出参数),寄存器之前要用两个%%(为了与占位符区分),其他也没什么
输入,输出参数列表
如果有多个输入,输出参数,则相互之间用','隔开;每一个参数形式如
/* 对于输入参数 */"描述符"(C++表达式/C++局部|全局变量)/* 对于输出参数 */"描述符"(左值)
对于输入参数来说,描述符只有一个字符,用于说明在调用'函数体'之前,应该将变量复制到哪里;对于输出参数来说,描述符一般有两个字符,说明了结束调用后,从哪里对变量赋值;如:
#include#define _mBeginASM __asm__ __volatile__ (#define _mEndASM );int main(int argc,char *argv[]){ int a=44,b=33,c; _mBeginASM "addl %%ebx,%%eax" :"=a"(c) /* 说明了调用'函数体'之后,应该把eax中的值赋值该变量c */ :"b"(b),"a"(a) /* 表明了在调用'函数体'之前,应该把变量a复制到eax中,b复制到ebx中 */ _mEndASM printf("%d\n",c); return 0;} /* 生成的汇编代码段 */ movl $44, -28(%rbp) movl $33, -24(%rbp) /* 变量a存放在 -28(%rbp) 中,b 存放在-24(%rbp)中 */ movl -24(%rbp), %ebx /* b->ebx */ movl -28(%rbp), %eax /* a->eax */ #APP /* 调用函数体... */# 16 "Hello.cpp" 1 addl %ebx,%eax# 0 "" 2#NO_APP movl %eax, -20(%rbp) /* eax->c;c 存放在-20(%rbp) */
完整的描述符表可以自行搜索...
r 表示任何可用的通用寄存器;
m 表示直接使用内存位置,即内存操作数
占位符:
_mBeginASM"imul %1,%2\n\t""movl %2,%0":"=r"(c):"r"(a),"r"(b)_mEndASM
作用机理就是:
r表示任何可用的通用寄存器,所以G++首先会为a,b,c选择一个通用寄存器;如对于a选择使用 eax;b选择使用ebx;c选择使用ecx;那么 %0 就是 ecx %1 就是 eax %2 就是 ebx 生成的汇编代码如下:movl -24(%rbp),eaxmovl -28(%rbp),ebx#APPimul %eax,%ebxmovl %ebx,%ecx#NO_APPmovl %ecx,-20(%rbp)
数字描述符
_mBeginASM"imul %1,%2":"=r"(c):"r"(a),"0"(b) /* "0"(b)表示为b分配的寄存器与%0(即c)一样,即 %2==%0 */_mEndASM
使用内存操作数
#include#define _mBeginASM __asm__ __volatile__ (#define _mEndASM );int main(int argc,char *argv[]){ int a=44,b=33,c; printf("%d\t%d\n",a,b); _mBeginASM "xchgl %1,%2" :"=r"(a) :"0"(a),"m"(b) _mEndASM printf("%d\t%d",a,b); return 0;}/* 生成的汇编代码: */ movl -4(%rbp), %eax /* a->eax */#APP# 15 "Hello.cpp" 1 xchgl %eax,-8(%rbp) /* -8(%rbp)就是b,m的意思就是使用内存操作数 */# 0 "" 2#NO_APP movl %eax, -4(%rbp) /* eax->a *//* 由于b使用内存操作数,所以不需要在调用之前把b移到某个寄存器中,也不需要在调用之后从某个寄存器中为b赋值 */
使用FPU
描述符,输出值不能用f来描述;
f 表示任何可用的浮点寄存器,即st(0)-st(7);
t 表示 st(0)
u 表示st(1)
#include#include #define _mBeginASM __asm__ __volatile__ (#define _mEndASM );#define _mGetSinCos(d,sind,cosd) \ /* 内联汇编通常与宏连载一起 */ do{ \ _mBeginASM \ "fsincos" \ :"=t"(cosd),"=u"(sind) \ :"0"(d) \ _mEndASM \ }while(0);int main(int argc,char *argv[]){ double sind,cosd; double d; while(scanf("%lf",&d) >= 1){ printf("%f\t%f\n",sin(d),cos(d)); _mGetSinCos(d,sind,cosd); printf("%f\t%f\n",sind,cosd); } return 0;}
注意:不要遗忘被修改的FPU寄存器
因为FPU的数据寄存器是以堆栈的形式工作的,所以不要忘记被修改的寄存器,如:
_mBeginASM"fild %1\n\t""fimul %1\n\t""fldpi\n\t""fmul %%st(1),%%st(0)\n\t":"=t"(area):"m"(r):"st(1)" /* 手动运行一下内联汇编指令的话,可以发现退出'函数体'时,st(1)被修改了 * 并且st(1)并没有出现在输入,输出列表中,所以应该在'改动的寄存器列表'部分声明一下 */_mEndASM
改动的寄存器列表
如果我们在内联汇编代码中,修改了某些寄存器的值,并且这些寄存器没有在输入,输出部分出现;那么我们应该在改动的寄存器列表中登记一下该寄存器
#include#define _mBeginASM __asm__ __volatile__ (#define _mEndASM );int main(int argc,char *argv[]){ int a=44,b=33,c; printf("%d\t%d\n",a,b); _mBeginASM "movl %2,%%r8d\n\t" "movl %3,%2\n\t" "movl %%r8d,%3" :"=r"(a),"=r"(b) :"0"(a),"1"(b) :"r8" _mEndASM /* 因为r8d被修改了,也即r8被修改了,所以要在'改动的寄存器列表'里声明一下 * 实际上..g++提示:r8d是未知的寄存器名,不过r8可以.. */ printf("%d\t%d",a,b); return 0;}
不过如果寄存器已经在输入,输出部分出现过一次了,则不需要在改动的寄存器列表中重新声明;
数字标签
条件跳转指令与无条件跳转指令都允许指定一个数字加上方向标志作为标签;处理器会根据方向标志向前后向后搜索,第一个遇到的数字标签会被采用。如:
#include#include #define _mBeginASM __asm__ __volatile__ (#define _mEndASM );#define _mMax(a,b,r) \ do{ \ _mBeginASM \ "cmp %1,%2\n\t" \ "jge 0f\n\t" \ "mov %1,%0\n\t" \ "jmp 1f\n" \ "0:\n\t" \ "mov %2,%0\n" \ "1:"\ :"=r"(r) \ :"r"(a),"r"(b) \ _mEndASM \ }while(0);int main(int argc,char *argv[]){ int a,b; int c; while(scanf("%d %d",&a,&b) >= 2){ _mMax(a,b,c); printf("%d\n",c); } return 0;}/* 生成的指令 */ movl -12(%rbp), %eax movl -8(%rbp), %edx#APP# 27 "Hello.cpp" 1 cmp %eax,%edx jge 0f mov %eax,%eax jmp 1f0: mov %edx,%eax1:# 0 "" 2#NO_APP movl %eax, -4(%rbp)
注意:
由于G++是直接将内联汇编插入到为程序生成的汇编代码中,所以标签'1'...
'f'表示向前搜索,'b'表示向后搜索;