当我妄图把写的代码移植到linux-gcc上时发现
"intrin.h"不存在
那么函数cpuid也不存在了,哭
PS: 其实还是有的,不过在 "cpuid.h" 里,但是man cpuid只介绍了这条汇编和一些设备相关的东西.....
那该怎么办呢?
内联汇编啊o(〃^▽^〃)o
于是踏上了编译器拓展这条不归路...
C++存在asm
关键字来允许程序中嵌入汇编代码。
GCC种的内联汇编分成两种,第一种是基础汇编(Basic Asm),这种形式的内联汇编不能使用GCC拓展的操作数(Operand),第二种呢就是拓展汇编(Extended Asm),能用GCC拓展的操作数...
GCC官方的建议是使用拓展汇编,因为这样性能更好
Using extended asm typically produces smaller, safer, and more efficient code, and in most cases it is a better solution than basic asm
但是拓展汇编只能在函数中使用(而且不能在裸函数中使用)
不论使用哪种汇编,其格式都是
asm asm-qualifiers ( ... )
对于基础汇编,直接把“...”换为汇编指令的字符串就行啦
扩展汇编么...会多一堆东西..后面再说
asm-qualifiers,就是汇编的限定符,分为三种
GCC(邪恶的)优化器会尝试优化内联汇编,甚至可能把某段内联汇编移除,不想遭此待遇,那么就要加上volatile
来防止优化,如果你的asm中含有一些side effects的话,这点就非常重要(举个例子 ,对某些单片机初始化会要求对寄存器连续赋值,那么赋值的顺序就十分重要,不能颠倒。但GCC认为汇编就是用输入来产生输出的,为了快速得到输出,会对顺序,甚至语句进行修改)。并且基础汇编隐式的包含此限定符
虽然优化器会对内联汇编进行操作,但是parser不会对内联汇编进行解析哦
使用inline
限定符,那么GCC会生成尽可能小的汇编代码。
goto
限定符只能对拓展汇编使用,提示编译器这段语句可能会跳转到外部的某处label,但是我认为这种用法过于邪恶所以只给出参考
这里先写一段简简单单的基础汇编--两个int相加
C的写法是
int funcAdd(int a,int b){
return a+b;
}
要把函数转换成汇编
此时参数a和参数b在哪个寄存器中呢....?
这时候就需要一点小小的知识♪(´▽`)
是什么控制着函数的参数传递呢?Calling Convention!
对32位下的C语言,其函数的默认Calling Convention是cdecl
这代表着其参数的传递方式是从右至左依次压栈(所以栈顶是第一个参数),返回值通过eax传递,而调用者负责清栈
于是可以写出这种代码来
int funcAdd(int a,int b){
asm volatile(
"movl %esp,%ebp\n\t"
"movl 8(%ebp),%edx\n\t"
"movl 12(%ebp),%eax\n\t"
"addl %edx,%eax\n\t"
);
}
int main(){
int t = funcAdd(3, 5);
printf("3 + 5 = %d\n",t);
}
output: 3 + 5 = 8
这段代码在gcc参数-m32
下能编译通过且运行成功,否则会出现segmentation fault。
这里可能会有点疑惑:
看一下外部调用的形式
打开反汇编发现在main函数中有这么一段
pushl $5
pushl $3
call funcAdd
addl $8, %esp
首先,这段代码会把两个参数从右向左依次压栈,接着执行 call
指令跳转到目标函数,在目标函数返回后(也就是内部调用了 ret
)后平栈。
在 Call
指令执行时,cpu会自动把ip压栈作为ret时的返回地址(所以ret时会pop ip),由于是32位系统,大小就是4Bytes。
GCC在不为人知的地方做了“努力”,函数的反汇编中会在开头发现两行神秘汇编
pushl %ebp
movl %esp, %ebp
这里GCC莫名奇妙为我们压栈了ebp,并且把esp赋值给了ebp。
其实是这样的,由于在程序运行中esp会自动增减(pop,push什么的),这样对于程序员来说不太好定位到目标。于是GCC会先保存栈基地址(也就是push ebp),然后把esp赋值给ebp,方便访问。
那么此时的栈内情况如下
+----------+
|..........|
|----------| 0x0000
| b |
|----------| -0x0020
| a |
|----------| -0x0040
| ip |
|----------| -0x0080
| ebp |
+----------+ -0x00C0 <--esp
x86的栈是向下生长的哦
这下一切都说的通了
上面展示的是基础汇编,这种写法很难与C的数据交互,很考验程序员的知识量,并且兼容性很成问题(从x86到x64都得大改一番
于是GCC提供了替代方案,拓展汇编。
拓展汇编的格式如下
asm asm-qualifiers ( AssemblerTemplate
: OutputOperands
: InputOperands
: Clobbers
: GotoLabels)
这里括号内不再是汇编指令了,取而代之的是汇编模板,以及一大堆的参数列表,输出操作数(OutputOperands),输入操作数(InputOperands),Clobbers(这个我真的翻译不来),GOTO标签。
操作数(Operands)极大的方便了汇编与C语言数据的交互,格式如下
[label] "constraints" (variable)
每个操作数都拥有约束符constraints
和其标签(可选)
在汇编模板中引用操作数的方法有两个,首先是利用操作数出现的位置N,然后用%N
来引用,或者是利用标签,用%[label]
来引用。
"constraints"
通常代表着操作数在汇编中的存放方式,在这里列出几个可能常用的
OutputOperands的约束必须以'='或者'+'开头
Clobbers含义比较复杂,大概就是:如果你的汇编修改了某些位置的值,但是没有在OutputOperands中被提到,那么就需要列在这里,用来提示编译器。
这里可以直接列出被修改的寄存器 (比如 "rax", "ebx",除了栈指针),如果内存被修改了那么应该填入"memory";"cc"如果修改了标志寄存器。
栗子时间\( ̄︶ ̄*\))
改写以下之前的加法函数
int funAdd(int a,int b){
int ret;
asm volatile(
"addl %1,%2\n\t"
"movl %2,%0\n\t"
: "=r"(ret)
: "0"(a),"r"(b)
:"cc"
//对于%0和%1,他们在汇编中表表示的变量是同一个
//只不过在执行时会读入a的值,输出的时候会覆写ret的值
);
return ret;
}
Very easy.
或者这样写
现在提升点难度,把结果输出到一个指针指向的内存中
void funAddP(int a,int b,int* p){
asm volatile(
"addl %1,%2\n\t"
"movl %2,%[out]\n\t"
: [out]"=m"(*p)
: "r"(a),"r"(b)
//上面三处也可用"rm",让编译器来帮你选个好位置
:"cc"
);
}
是不是觉得GCC的拓展也就那么回事,简简单单<(^-^)>
回到最开始的需求 --- 如何用汇编写一个cpuid函数?
void CpuChecker::m_cpuid(int registers[4], int eax, int ecx = 0) {
#if defined (__GNUC__)
asm volatile ("CPUID"
: "=a" (registers[0]),"=b" (registers[1]), "=c" (registers[2]), "=d" (registers[3])
: "a" (eax), "c" (ecx)
);
#else
__cpuidex(registers, eax, ecx);
#endif
}
首先,把输入分别放入eax,ecx,然后执行cpuid。
此时返回值应该被存在eax,ebx,ecx,edx,于是加上constraint来让值从目标寄存器写回数组里,收工。
6.47 How to Use Inline Assembly Language in C Code - gcc.gnu.org