C语言内联汇编使用方法

GCC内联汇编

一、基本语法

asm volatile ( assembler template
     : output operands				/* optional */
     : input operands				/* optional */
     : list of clobbered registers  /* optional */
     );
(1) 关键字asm和volatile

asm为GCC关键字,表示接下来要嵌入汇编代码,如果asm与其他程序中的命名冲突,可以使用__asm__

volatile为可选关键字,表示不需要对汇编代码做任何优化,类似的,也支持__volatile__

(2) 汇编指令

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。

(3) 输出操作数

内联汇编由零个或者多个输出操作数,用来指示内联汇编指令修改了C代码中的变量,则需要对每个输出参数进行分隔,格式为:

[[asmSymbollicName]]	constraint	(cvariablename)

输出操作数指定名字asmSymbollicName,汇编指令可以使用这个名字引用输出操作数。

除了使用名字引用操作数外,还可以使用序号引用操作数,比如操作数有两个,%0引用第一个操作数,%1引用第二个操作数,依次类推。

输出操作数必须用”=“或者”+“作为前缀,=表示只写,+表示读写。前缀之后就可以是各种约束,比如”=a"表示将结果输出至rax/eax寄存器,然后再由rax/eax寄存器更新相应的输出。

cvariablename为代码中C变量的名字,需要使用括号括起来。

(4) 输入操作数

内联函数可以由零个或者多个输入操作数,操作数来自C代码中的变量或者是表达式,作为汇编指令的输入,每个输入操作数的格式如下:

[[asmSymbollicName]]	constraint	(cvariablename)

参数可参照输出操作数,但是序号操作数为最后一个输出操作数加1开始,依次类推。

(5) clobber list

某些汇编指令执行之后有一些副作用,可能会隐性地影响某些寄存器或者是内存的值,如果被影响的寄存器或者是内存并没有在输入、输出操作数中列出来,那么需要将这些寄存器或者内存列入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中。

二、使用实例

(1) 简单求和
#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赋值的汇编代码进行解释:

  • “b”是输出操作数,由 %0 引用,“a”是输入操作数,由 %1 引用。
  • “r”是对操作数的约束。"r”告诉 GCC 使用任何寄存器来存储操作数。输出操作数约束应该有一个约束修饰符“=”。这个修饰符表示它是输出操作数并且是只写的。
  • 寄存器名称前有两个 % 前缀。这有助于 GCC 区分操作数和寄存器。操作数有一个 % 作为前缀。
  • 第三个冒号后被破坏的寄存器 %eax 告诉 GCC %eax 的值将在“asm”中修改,因此 GCC 不会使用这个寄存器来存储任何其他值。

当“asm”的执行完成时,“b”将反映更新后的值,因为它被指定为输出操作数。换句话说,在“asm”内部对“b”所做的更改应该反映在“asm”之外。

(2) 内核中idt desc_ptr获取实例
/*
 * 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表的地址。

(3) SGDT/SIDT-存储全局/中断描述附表寄存器

操作码 | 指令 | 说明

操作码 指令 说明
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 指令仅在操作系统软件中有用;不过它们也可以在应用程序中使用,而不会导致生成异常。

(4) 内联汇编实现getpid系统调用

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;
}
(5) x86_64内核cr0寄存器读写操作
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));
}

你可能感兴趣的:(Linux,C,linux,c语言)