asm volatile ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
asm为GCC关键字,表示接下来要嵌入汇编代码,如果asm与其他程序中的命名冲突,可以使用__asm__
。
volatile为可选关键字,表示不需要对汇编代码做任何优化,类似的,也支持__volatile__
C语言中内联汇编代码必须用双引号将命令括起来,如果是多行汇编指令,则每条指令占用一行,每行指令用双引号括起来,以后缀\t\n结尾,\n表示newline的缩写,\t表示tab的缩写。
如示例代码:
__asm__ __volatile__ ("movl %eax, %ebx \n\t"
"movl $100, %esi \n\t"
"movl %ecx, $label(%edx, %ebx, $4) \n\t"
"movb %ah, (%ebx) \n\t");
当使用拓展模式(包含output,input,clobbered部分),汇编指令中需要使用两个%来引用寄存器,比如%%rax;使用一个%来引用输入、输出参数,比如%1。
内联汇编由零个或者多个输出操作数,用来指示内联汇编指令修改了C代码中的变量,则需要对每个输出参数进行分隔,格式为:
[[asmSymbollicName]] constraint (cvariablename)
输出操作数指定名字asmSymbollicName,汇编指令可以使用这个名字引用输出操作数。
除了使用名字引用操作数外,还可以使用序号引用操作数,比如操作数有两个,%0引用第一个操作数,%1引用第二个操作数,依次类推。
输出操作数必须用”=“或者”+“作为前缀,=表示只写,+表示读写。前缀之后就可以是各种约束,比如”=a"表示将结果输出至rax/eax寄存器,然后再由rax/eax寄存器更新相应的输出。
cvariablename为代码中C变量的名字,需要使用括号括起来。
内联函数可以由零个或者多个输入操作数,操作数来自C代码中的变量或者是表达式,作为汇编指令的输入,每个输入操作数的格式如下:
[[asmSymbollicName]] constraint (cvariablename)
参数可参照输出操作数,但是序号操作数为最后一个输出操作数加1开始,依次类推。
某些汇编指令执行之后有一些副作用,可能会隐性地影响某些寄存器或者是内存的值,如果被影响的寄存器或者是内存并没有在输入、输出操作数中列出来,那么需要将这些寄存器或者内存列入clobber list中。
执行一个加法运算将val + 400的计算结果放入sum,sum = val + 400:
int val = 100, sum = 0;
asm ("movl %1, %%rax; \n\t"
"movl %c[addend], %%rbx; \n\t"
"addl %%rbx, %%rax; \n\t"
"movl %%rax, %0"
: "=" (sum)
: (c)(val), [addend] " i " (400)
: "rbx"
);
第三行:因为存在寄存器引用和通过需要引用的操作数,所有使用两个”%“来引用寄存器,%1引用的是第一个输入操作数val,其中c表示使用rcx的寄存器保存val,然后将rcx的值赋值给rax。
第四行:引用addend是第二个输入操作数符号的名字,因为这是一个立即数,所以这个变量前面使用了c修饰符,这个是GCC语法,表示后面是一个立即数。
第五行:将rbx 和 rax寄存器的值相加,结果存放在rax中。
第六行:%0引用的是第一个输出操作数sum,将rax的值赋值给sum,因为sum只是输出操作数,所以只用"="修饰。
这段代码可以看到,在汇编代码中使用了rbx寄存器,但是rbx并没有出现在输入和输出操作数中,所以内联汇编需要把rbx寄存器放入clobber list中。
#include
#include
int main(void)
{
/* basic command demo */
__asm__("movl %eax, %ecx");
/* set b = 10 */
int a = 10, b = 0;
__asm__("movl %1, %%eax;"
"movl %%eax, %0;"
:"=r" (b) /* output */
:"r" (a) /* input */
:"%eax" /* clobbered register */
);
printf("%s: b = %d\n", __func__, b);
return 0;
}
以上就是基本Linux c语言内联汇编的基本语法,可以参考下,下面对b赋值的汇编代码进行解释:
当“asm”的执行完成时,“b”将反映更新后的值,因为它被指定为输出操作数。换句话说,在“asm”内部对“b”所做的更改应该反映在“asm”之外。
/*
* Override for the debug_idt. Same as the default, but with interrupt
* stack set to DEFAULT_STACK (0). Required for NMI trap handling.
*/
struct desc_ptr {
unsigned short size;
unsigned long address;
} __attribute__((packed)) ;
const struct desc_ptr debug_idt_descr = {
.size = IDT_ENTRIES * 16 - 1,
.address = (unsigned long) debug_idt_table,
};
static inline void store_idt(struct desc_ptr *dtr)
{
asm volatile("sidt %0":"=m" (*dtr));
}
#include
#include
struct desc_ptr {
unsigned short size;
unsigned long address;
} __attribute__((packed)) ;
int main(void)
{
/* ring3 get idt table address */
struct desc_ptr idt_ptr = {0};
__asm__("sidt %0":"=m" (idt_ptr));
printf("%s: the desc_ptr address is 0x%lx\n", __func__, idt_ptr.address);
return 0;
}
这里也就是通过汇编指令sidt获取struct desc_ptr的地址,然后再访问idt表的地址。
操作码 | 指令 | 说明
操作码 | 指令 | 说明 |
---|---|---|
OF 01 /0 | SGDT m | 将GDTR存储到m |
OF 01 /1 | SITD m | 将IDTR存储到m |
将全局描述符表格寄存器 (GDTR) 或中断描述符表格寄存器 (IDTR) 中的内容存储到目标操作数。目标操作数是指定 6 字节内存位置。如果操作数大小属性为 32 位,则寄存器的 16 位限制字段存储到内存位置的 2 个低位字节,32 位基址存储到 4 个高位字节。如果操作数大小属性为 16 位,则限制字段存储在 2 个低位字节,24 位基址存储在第三、四及五字节,第六字节使用 0 填充。
SGDT 与 SIDT 指令仅在操作系统软件中有用;不过它们也可以在应用程序中使用,而不会导致生成异常。
getpid系统调用的作用是获取当前进程的进程号。系统调用采用eax传递系统调用号。getpid的系统调用号是20,也就是要将调用号20存入寄存器eax。然后执行系统调用是通过执行int $0x80。
#include
#include
int main(void)
{
unsigned int pid = getpid();
printf("%s: pid is %d\n", __func__, pid);
return 0;
}
#include
#include
int main()
{
unsigned int pid;
asm volatile(
"mov $0,%%ebx\n\t"
"mov $20,%%eax\n\t"
"int $0x80\n\t"
"mov %%eax,%0\n\t"
:"=m" (pid)
);
printf("pid is %u\nget pid by asm\n",pid);
return 0;
}
static inline unsigned long read_cr0(void)
{
unsigned long cr0;
asm volatile("movq %%cr0,%0" : "=r" (cr0));
return cr0;
}
static inline void write_cr0(unsigned long val)
{
asm volatile("movq %0,%%cr0" :: "r" (val));
}