内联汇编语法说明

本文深入探讨GCC内联汇编的使用原理及格式,解释如何在C代码中嵌入汇编指令,处理变量与寄存器的映射,以及如何指定被修改的资源。通过具体示例,阐述内联汇编在解决特定问题上的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么使用内嵌汇编?

——解决一些无法直接用C或C++实现的功能,比如C中没有现成的函数或语法可用。


内联汇编的使用原理:

在内嵌汇编中,可以将C语言变量指定为汇编指令的操作数,而且不用去管如何将C语言变量的值读入哪个寄存器,以及如何将计算结果写回C变量,你只要告诉程序中C语言变量与汇编指令操作数之间的对应关系即可, GCC会自动插入代码完成必要的操作。  

使用内嵌汇编,要先编写汇编指令模板,然后将C语言变量与指令的操作数相关联,并告诉GCC对这些操作有哪些限制条件。


首先,让我们来共同了解一下 GCC 内联汇编的一般格式:
asm(
代码列表
: 输出运算符列表
: 输入运算符列表
: 被更改资源(破坏描述)列表
);

在代码列表中,每个汇编语句都要用"  "括起来。

例:
__asm__ __violate__ 
("movl %1,%0" : "=r" (result) : "m" (input)); 

简单说明:
         在 C 代码中嵌入汇编需要使用 asm 关键字,用法asm();
        "  "      引号内部包含的部分是指令部分
        :            参数输出部分     函数的返回值,表示这段汇编执行完之后,哪些寄存器用于存放输出数据,而这些寄存器会分别对应一C语言表达式值或一个内存地址,会将这些寄存器的值输出到C语言中的变量中去;
        :            参数输入部分     函数的形参,表示在开始执行汇编代码时,这里指定的一些寄存器中应存放的输入值,它们也分别对应着一C变量或常数值;
        :            被更改资源列表             内联汇编的声明部分,要被更改的资源。有时在进行某些操作时,除了要用到进行数据输入和输出的寄存器外,还要使用多个寄存器来保存中间计算结果,这样就难免会破坏原有寄存器的内容。如果这条内联汇编执行完之后该被破坏寄存器的值继续被其他程序使用,那么就会导致错误。所以我们需要在“破坏描述部分”声明这些寄存器或内存。若寄存器被列入这个列表,则等于是告诉gcc,这些寄存器可能会被内联汇编命令改写。因此,执行内联汇编的过程中,需要先将这些寄存器入栈,等执行完之后再恢复。例如,某一时刻某进程使用了寄存器ecx,并存入10,然后你插队执行你的内联汇编程序修改了ecx寄存器的值,这将导致其他的程序出现问题。那么如果你将ecx加入破坏描述部分,gcc会在使用ecx寄存器前先push入栈,等使用完ecx后再pop回去。这就保证了ecx寄存器在使用过程中没有被修改。另外,如果某寄存器在内联汇编执行过程中被隐式地使用到,它既不在输出又不在输入列表中声明,那么gcc可能将这个寄存器分配给某个c变量,这使得gcc分配的寄存器与代码列表中隐式地用到的寄存器碰巧为同一个寄存器,导致错误。

1.
        "r"     用寄存器来保存参数
        "i"     是立即数
        "m"   一个有效的内存地址
        "x"    只能做输入

        +  :     表示参数的可读可写
        无:    表示参数只读
        =  :     表示只写
        & :     只能做输出

2. 
    %0  输出列表和输入列表的第1个成员
    %1  输出列表和输入列表的第2个成员
    %2  输出列表和输入列表的第3个成员
   ...  依次类推

3. 冒号部分可以省略,要省略全部省略,否则全部写上


更多内容可参考其他链接:

https://blog.youkuaiyun.com/lin111000713/article/details/18892449

https://www.linuxprobe.com/gcc-how-to.html


例子详解:

__asm__ __violate__ 
("movl %1,%0" : "=r" (result) : "r" (input): "r0", "r1"); 

1)“movl %1,%0”是指令模板;“%0”和“%1”代表指令的操作数,称为占位符,内嵌汇编靠它们将C语言表达式与指令操作数相对应。

2)用小括号括起来的是C 语言变量,本例中只有两个:“result”和“input”,他们按照出现的顺序分别与指令操作数“%0”,“%1,”对应;注意对应顺序:第一个C变量对应“%0”;第二个变量对应“%1”,依次类推,操作数至多有10个,分别用“%0”,“%1”….“%9,”表示。

在每个操作数前面有一个用引号括起来的字符串,字符串的内容是对该操作数的限制或者说要求。

“result”前面的限制字符串是“=r”,其中“r”表示需要将“result”与某个通用寄存器相关联,先将操作数的值读入寄存器,然后在指令中使用相应寄存器,而不是“result”本身,当然指令执行完后需要将寄存器中的值存入变量“result”,从表面上看好像是指令直接对“result”进行操作,实际上GCC做了隐式处理,这样我们可以少写一些指令。“=”表示“result”是输出操作数:即在汇编里只能改变该C变量的值,而不能取它的值。+号表示可以取变量值,也可改变变量的值。

3)在参数输入部分,不能再有"=","+"号,表示汇编里只能读c变量的值。“input”前面的“r”表示该表达式需要先放入某个寄存器,然后在指令中使用该寄存器参加运算。 

4)最后一部分(第三个冒号后)表示告诉编译器不要把r0, r1寄存器分配给%0, %1等。


详细说明:

1. 限制字符含义汇总:

每个操作字前面双引号内的限制字符有很多种,有些是与特定体系结构相关,此处仅列出常用的限定字符和i386中可能用到的一些常用的限定符。它们的作用是指示编译器如何处理其后的 C 语言变量与指令操作数之间的关系。 

分类 限定符描述 
通用寄存器 “a” 将输入变量放入eax 
“b” 将输入变量放入ebx 
“c” 将输入变量放入ecx 
“d” 将输入变量放入edx 
“s” 将输入变量放入esi 
“d” 将输入变量放入edi 
“q” 将输入变量放入eax,ebx,ecx,edx中的一个 
“r” 将输入变量放入通用寄存器,即eax,ebx,ecx,edx,esi,edi之一 
“A” 把eax和edx合成一个64 位的寄存器(use long longs) 
内存 “m” 内存变量 
“o” 操作数为内存变量,但其寻址方式是偏移量类型, 也即基址寻址 
“V” 操作数为内存变量,但寻址方式不是偏移量类型 
“ ” 操作数为内存变量,但寻址方式为自动增量 
“p” 操作数是一个合法的内存地址(指针) 
寄存器或内存 “g” 将输入变量放入eax,ebx,ecx,edx之一,或作为内存变量 
“X” 操作数可以是任何类型 
立即数 “I” 0-31之间的立即数(用于32位移位指令) 
“J” 0-63之间的立即数(用于64位移位指令) 
“N” 0-255之间的立即数(用于out指令) 
“i” 立即数 
“n” 立即数,有些系统不支持除字以外的立即数,则应使用“n”而非 “i” 
匹配 “ 0 ” 表示用它限制的操作数与某个指定的操作数匹配 
“1” ...也即该操作数就是指定的那个操作数,例如“0” 
“9” 去描述“%1”操作数,那么“%1”引用的其实就是“%0”操作数,注意作为限定符字母的0-9 与指令中的“%0”-“%9”的区别,前者描述操作数, 后者代表操作数。 
该输出操作数不能使用过和输入操作数相同的寄存器 
操作数类型 “=” 操作数在指令中是只写的(输出操作数)  
“+” 操作数在指令中是读写类型的(输入输出操作数) 
浮点数 “f” 浮点寄存器 
“t” 第一个浮点寄存器 
“u” 第二个浮点寄存器 
“G” 标准的80387浮点常数 
该操作数可以和下一个操作数交换位置,例如addl的两个操作数可以交换顺序(当然两个操作数都不能是立即数) 
部分注释,从该字符到其后的逗号之间所有字母被忽略 
表示如果选用寄存器,则其后的字母被忽略 

2. 被更改资源列表:

有时在进行某些操作时,除了要用到进行数据输入和输出的寄存器外,还要使用多个寄存器来保存中间计算结果,这样就难免会破坏原有寄存器的内容。如果希望GCC在编译时能够将这一点考虑进去。那么你就可以在“破坏描述部分”声明这些寄存器或内存。
 这种情况一般发生在一个寄存器出现在“汇编语句模板”,但却不是由输入或输出操作表达式所指定的,也不是在一些输入或输出操作表达式使用"r"、"g"约束时由GCC为其选择的,同时此寄存器被“汇编语句模板”中的指令修改,而这个寄存器只是供当前内嵌汇编临时使用的情况。比如:
__asm__("movl %0, %%ebx" : : "a"(foo) : "%ebx");
寄存器%ebx出现在“汇编语句模板”中,并且被movl指令修改,但却未被任何输入或输出操作表达式指定,所以你需要在“破坏描述部分”指定"%ebx",以让GCC知道这一点。
因为你在输入或输出操作表达式所指定的寄存器,或当你为一些输入或输出操作表达式使用"r"、"g"约束,让GCC为你选择一个寄存器时,GCC对这些寄存器是非常清楚的——它知道这些寄存器是被修改的,你根本不需要在“破坏描述部分”再声明它们。但除此之外,GCC对剩下的寄存器中哪些会被当前的内嵌汇编修改一无所知。所以如果你真的在当前内嵌汇编语句中修改了它们,那么就最好“破坏描述部分”中声明它们,让GCC针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。
在“破坏描述部分”中指定这些寄存器的方法很简单,你只需要将寄存器的名字使用双引号引起来。如果有多个寄存器需要声明,你需要在任意两个声明之间用逗号隔开。比如:
__asm__("movl %0, %%ebx; popl %%ecx" : : "a"(foo) : "%ebx", "%ecx" );
注意准备在“破坏描述部分”声明的寄存器必须使用完整的寄存器名称,在寄存器名称前面使用的“%”是可选的。
另外需要注意的是,如果你在“破坏描述部分”声明了一个寄存器,那么这个寄存器将不能再被用做当前内嵌汇编语句的输入或输出操作表达式的寄存器约束,如果输入或输出操作表达式的寄存器约束被指定为"r"或"g",GCC也不会选择已经被声明在“破坏描述部分”中的寄存器。比如:
__asm__("movl %0, %%ebx" : : "a"(foo) : "%eax", "%ebx");
此例中,由于输出操作表达式"a"(foo)的寄存器约束已经指定了%eax寄存器,那么再在“破坏描述部分”中指定"%eax"就是非法的。编译时,GCC会给出编译错误。

除了寄存器的内容会被改变,内存的内容也可以被修改。如果一个“汇编语句模板”中的指令对内存进行了修改,或者在此内嵌汇编出现的地方内存内容可能发生改变,而被改变的内存地址你没有在其输出操作表达式使用"m"约束,这种情况下你需要在“破坏描述部分”使用字符串"memory"向GCC声明:“在这里,内存发生了或可能发生了改变”。例如:
void * memset(void * s, char c, size_t count)
{
__asm__("cld\n\t"
"rep\n\t"
"stosb"
: /* no output */
: "a"(c), "D"(s), "c"(count)
: "%ecx", "%edi", "memory");


return s;
}
此例实现了标准函数库memset,其内嵌汇编中的stosb对内存进行了改动,而其被修改的内存地址s被指定装入%edi,没有任何输出操作表达式使用了"m"约束,以指定内存地址s处的内容发生了改变。所以在其“破坏描述部分”使用"memory"向GCC声明:内存内容发生了变动。
如果一个内嵌汇编语句的“破坏描述部分”存在"memory",那么GCC会保证在此内嵌汇编之前,如果某个内存的内容被装入了寄存器(通常因为编译器优化,会将某个内存处的变量缓存到某寄存器中来使用),那么在这个内嵌汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝。编译器在优化代码时,将内存的内容放到寄存器中去使用,而我们的内联汇编改变了该内存处的值,如果不告诉编译器,它是意识不到这一点的,就一直把寄存器中的内容当作内存内容来使用,这就与我们本来的意图不一致了。

当一个“汇编语句模板”中包含影响eflags寄存器中的条件标志,那么需要在“破坏描述部分”中使用"cc"来声明这一点。这些指令包括adc,div,popfl,btr,bts等等,另外,当包含call指令时,由于你不知道你所call的函数是否会修改条件标志,为了稳妥起见,最好也使用"cc"。


JOS内核中用到的例子:

1. 在启动kernel并从内核态进入用户态执行用户进程时,需要使用env_pop_tf(struct Trapframe *tf)函数,它的实现就是使用内联汇编:

kern/env.c中:

void env_pop_tf(struct Trapframe *tf)
{
    asm volatile(
        "\t movl %0, %%esp\n"       /* %0对应后面的tf,这里是将tf这个地址值赋给%esp */
        "\t popal\n"                /* 按*/
        "\t popl %%es\n"            /* 弹出值到%es */
        "\t popl %%ds\n"            /* 弹出值到%ds */
        "\t addl $0x8, %%esp\n"     /* 跳过tf_trapno 和 tf_errcode */
        "\t iret\n"                 /* 从中断返回,将栈中存储数据弹出到eip, cs, eflags寄存器中 */
        : : "g"(tf) : "memory");    /* “g”表示将输入变量tf放入eax,ebx,ecx,edx之一,或作为内存变量 */
                                    /* 告诉编译器在执行期间会发生内存变动,以防止错误的代码优化 */
    panic("iret failed");
}

2. 在用户代码进行系统调用时,需要用到一个统一的系统调用接口,它的实现就是通过内联汇编:

lib/syscall.c中:
/*
* 在JOS中所有系统调用通过syscall这个函数进行:执行int T_SYSCALL,把函数参数存入若干指定的寄存器
* 并指定函数返回值返回到寄存器ax中
* 用第一个参数num来确定到底是哪个系统调用
* 参数num == SYS_cputs,check == 0,a1 == b->buf, a2 == b->idx,剩下a3、a4、a5都为0
*/
static inline int32_t
syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
    int32_t ret;
    asm volatile("int %1\n"            //汇编指令模板,%1是占位符,对应后面的T_SYSCALL
                 : "=a" (ret)          //=表示在汇编里只能改变该C变量的值,而不能取它的值
                                       //ret值与%ax相联系,即指令执行完后ax的值存入变量ret
                 : "i" (T_SYSCALL),    //中断向量T_SYSCALL,是立即数
                   "a" (num),          //输入参数num,指令执行前先将num变量的值存入%ax
                   "d" (a1),           //输入参数a1,指令执行前先将a1变量的值存入%dx
                   "c" (a2),           //参数a2存入%cx
                   "b" (a3),           //参数a3存入%bx
                   "D" (a4),           //参数a4存入%di
                   "S" (a5),           //参数a5存入%si
                 : "cc", "memory");    //向gcc声明在这条汇编语言执行后,标志寄存器eflags和内存可能发生改变
                                       //加入“memory”,告诉GCC内存已经被修改,GCC得知这个信息后, 
                                       //就会在这段指令之前,插入必要的指令将前面因为优化缓存到寄存器中
                                       //的变量值先写回内存,如果以后又要使用这些变量再重新读取。
    if(check && ret > 0)
        panic("syscall %d returned %d (> 0)", num, ret);
    return ret;
}

3. 在kern/trap.c中的void trap(struct Trapframe *tf)函数有一段内联汇编:

void trap(struct Trapframe *tf)
{
    asm volatile("cld" ::: "cc");
    ...
}
//cld:clear direction flag:将标志寄存器Flag的方向标志位DF清零。
//在字串操作中使变址寄存器SI或DI的地址指针自动增加,字串处理由前往后。

//cli:clear interupt flag:将标志位IF清零,表示禁用中断。

 

### 如何在 Visual Studio 中配置内联汇编语法 在 Microsoft 的 Visual Studio (VS) 环境下,支持通过 `__asm` 关键字来编写内联汇编代码。然而需要注意的是,这种功能仅适用于 x86 和 x64 架构下的项目设置[^5]。对于 ARM 或其他架构,可能需要借助外部工具链(如 Clang/LLVM),或者直接使用独立的汇编文件。 以下是关于如何在 VS 中配置并使用内联汇编的具体说明: #### 1. 启用内联汇编的支持 为了能够在 Visual Studio 中启用内联汇编功能,必须确保项目的属性已正确配置: - 打开目标项目的 **属性页** (`Project Properties`)。 - 导航到路径: `Configuration Properties -> C/C++ -> Code Generation`. - 将选项 **Enable Minimal Rebuild** 设置为 `No (/Gm-)`。 - 如果未找到此选项,请确认当前平台是否为 Win32/x86 或 x64;ARM 平台不支持内联汇编。 #### 2. 使用 `__asm` 编写内联汇编代码 Visual Studio 支持两种方式书写内联汇编代码: - 单行形式:当只需要一条简单的汇编指令时,可以将其直接跟随 `__asm` 关键字之后。 - 多行形式:多条汇编指令需要用 `{}` 包裹起来,形成一个代码块。 下面是一个单行和多行的例子: ```cpp // 单行内联汇编 int a = 5; __asm mov eax, a; // 多行内联汇编 __asm { xor ebx, ebx inc ebx } ``` 注意,在某些复杂场景中,推荐将整个 `__asm` 块放入括号 `( )` 中以提高可读性和兼容性。 #### 3. 调试与测试 完成上述配置后即可运行调试程序验证效果。如果遇到错误提示,可能是由于以下原因造成的: - 当前活动架构不是 x86 或 x64; - 工程缺少必要的权限或依赖项; - 汇编语法存在拼写或其他逻辑问题。 针对这些问题可以通过调整解决方案平台以及仔细检查源码解决。 --- ### 注意事项 尽管 Visual Studio 提供了便捷的方式用于嵌入汇编代码,但在实际开发过程中仍需谨慎考虑其适用范围。例如现代处理器优化技术已经非常成熟,通常无需手动介入底层操作就能获得良好的性能表现[^3]。因此除非确实必要,否则应优先采用高级别的编程手段解决问题。 此外值得注意的是,随着软件工程实践的发展趋势变化,越来越多开发者倾向于利用跨平台工具链比如 GCC 或 LLVM 来处理复杂的混合语言需求[^2]。这不仅有助于增强移植能力还简化维护成本。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值