希望通过学习弄清楚如下问题:
- 什么内联?什么是内联汇编?
- 内联有什么优缺点?可以用来解决什么问题?
- 什么是 base asm?
- 什么是 extended asm?
- 都有 base asm了,为什么要extended asm?是为了解决什么问题呢?
文章目录
下面是对GCC官方文档C语言内联汇编部分尝试的原创翻译,错漏及理解不足之处在所难免,欢迎留言交流,我会尽快反馈。
6.47.2 Extended Asm - Assembler Instructions with C Expression Operands
extended asm
使得我们可以在汇编程序中读写C变量,还可以从汇编代码中跳转到C标签。extended asm 语法使用冒号':'
分割 assembler template 之后的操作数:
asm asm-qualifiers ( AssemblerTemplate
: OutputOperands
[ : InputOperands
[ : Clobbers ] ])
asm asm-qualifiers ( AssemblerTemplate
:
: InputOperands
: Clobbers
: GotoLabels)
上面第二个格式中,asm-qualifiers 包含 goto
(第一种格式不包含)([ ]表示可选)。
asm 关键字是一个GNU扩展。当你编写的代码将会使用 -ansi 和不同的 -std 选项进行编译时,请使用 __asm__ 代替 asm(参看 Alternate Keywords)
curiousXia:-ansi -std配置指向不同的C语言版本,当程序需要兼容多个C版本时,建议编码时将asm替换成__asm__。
Qualifiers(限定符)
-
volatile
extended asm 语句的典型用途是操作输入值产生输出值。但是,有时候我们的asm语句可能产生副作用。如此,我们便需要使用 volatile 限定符来禁用某些优化(以避免这些副作用的产生)。参看 Volatile。 -
inline
如果使用 inline 限定符,那么出于内联的目的,asm 语句的大小需要尽可能小(参看 Size of an asm )。 -
goto
这个限定符告知编译器,asm 语句将可能会跳转到 GotoLabels 标签列表中的某个标签。
Parameters(参数)
-
AssemblerTemplate
汇编模板,表示汇编代码的文字字符串。它是固定文本和标记的组合,这些标记引用输入、输出和goto参数。后面有详细介绍。 -
OutputOperands
输出操作数,逗号分隔的C变量列表,这些C变量被AssemblerTemplate中的汇编指令修改。列表允许为空。后面有详细介绍。 -
InputOperands
输入操作数,逗号分隔的C表达式列表,这些C表达式由AssemblerTemplate中的汇编指令读取。列表允许为空。后面有详细介绍。 -
Clobbers
逗号分隔的寄存器或值(由AssemblerTemplate更改的,除去列为输出的那些值)列表。列表允许为空。后面有详细介绍。 -
GotoLabels
goto标签。当使用 goto 格式的 asm 语句时,这部分是AssemblerTemplate汇编语言跳转到的C标签列表。asm 语句可能不会跳转到其他 asm 语句,而只会跳转到列出的 GotoLabels。GCC的优化器不了解其他跳转,因此在决定如何优化时无法考虑它们。
输入+输出+goto操作数的总数上限是30
。
Remarks(备注)
asm语句使得可以在C代码中直接包含汇编指令。这帮助我们在时间敏感的代码中最大化程序性能,或者访问C程序不太容易使用的汇编指令。
curiousXia:汇编语言是最底层的语言,直接与机器打交道,性能比C语言更好。操作系统中对于执行频率很高的C代码,使用asm语句嵌入汇编,可以提高程序的运行效率,进而提升性能表现。
注意
,extended asm 语句必须在函数内部。只有 basic asm 可以在函数外部。使用 naked 属性定义的函数也需要使用 basic asm(参看 Function Attributes)。
尽管 asm 的用法多种多样,但将asm语句视为一系列将输入参数转换为输出参数的低级指令可能会有所帮助。 一个在i386机器上使用asm的简单示例(如果不是特别有用)可能是这样:
int src = 1;
int dst;
asm ("mov %1, %0\n\t"
"add $1, %0"
: "=r" (dst)
: "r" (src));
printf("%d\n", dst);
这段代码将 src 的值拷贝给 dst,并且将 dst 加1。
curiousXia:这里使用的是哪种格式?
6.47.2.1 Volatile
有时候,GCC中的优化器会判定 asm 语句对输出变量没有必要并丢弃掉它。而且,如果优化器认为某段代码将永远返回同样的结果时(例如没有一个输入的值在调用过程中发生变化),它会将这部分代码从循环体中移出。使用 volatile 限定符关闭这些优化功能。那些没有输出参数的 asm 语句,包括 asm goto 语句,是隐式易失的(implicitly volatile)。
curiousXia:volatile 是不定的,不稳定的,易失的意思。代码中使用这个限定符就是告诉编译器:嘿,编译器老哥,这段代码的行为和结果是不确定的哦,编译时请你不要自作主张认为它的行为或结果是确定的然后一顿秀操作(优化),到时候我哭都来不及了~,拜谢~
下面这段i386机器的代码演示了一种不使用(或不需要) volatile 限定符的情况。如果它正在执行断言检查,那么这段代码使用 asm 执行验证;否则,不会有任何代码引用 dwRes。结果是,优化器丢弃 asm 语句,消除了对整个 DoCheck 例程的需求。通过在不需要时省略 volatile 限定符,可允许优化器生成尽可能高效的代码。
void DoCheck(uint32_t dwSomeValue)
{
uint32_t dwRes;
// Assumes dwSomeValue is not zero.
asm ("bsfl %1,%0"
: "=r" (dwRes)
: "r" (dwSomeValue)
: "cc");
assert(dwRes > 3);
}
下面这个例子展示的情形是,优化器认识到输入参数(dwSomeValue)在函数执行期间不会改变,因此将asm语句移出到for循环外面,以此产生更高效的代码。同样,可以使用 valatile限定符来关闭这类优化。
void do_print(uint32_t dwSomeValue)
{
uint32_t dwRes;
for (uint32_t x=0; x < 5; x++)
{
// Assumes dwSomeValue is not zero.
asm ("bsfl %1,%0"
: "=r" (dwRes)
: "r" (dwSomeValue)
: "cc");
printf("%u: %u %u\n", x, dwSomeValue, dwRes);
}
}
下面这个例子演示了需要使用 volatile 限定符的情况。其中用到了 x86 rdtsc 指令,这个指令可以用来读计算机时间戳计数器。如果没有用 volatile 限定符的话,优化器可能假定 asm 部分永远返回同样的值并因此将第二个调用优化掉。
uint64_t msr;
asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX.
"shl $32, %%rdx\n\t" // Shift the upper bits left.
"or %%rdx, %0" // 'Or' in the lower bits.
: "=a" (msr)
:
: "rdx");
printf("msr: %llx\n", msr);
// Do other work...
// Reprint the timestamp
asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX.
"shl $32, %%rdx\n\t" // Shift the upper bits left.
"or %%rdx, %0" // 'Or' in the lower bits.
: "=a" (msr)
:
: "rdx");
printf("msr: %llx\n", msr);
GCC的优化程序不会像前面示例中的非易失性代码那样对待这部分代码。 他们不会将其移出循环,也不会在前一次调用的结果仍然有效的前提下忽略它。
注意,编译器甚至可以相对于其他代码移动易失性asm指令,包括跨跳转指令。例如,在许多目标机器上,都有一个系统寄存器来控制浮点运算的舍入模式。 如下面的PowerPC示例中那样,使用易失的asm语句设置,将不能可靠地工作。
asm volatile("mtfsf 255, %0" : : "f" (fpenv));
sum = x + y;
编译器会将加法操作移回volatile asm语句之前。 为了使其按预期工作,我们添加一个虚假的依赖( “=X” (sum) )到asm中,并在接下来的代码中引用这个变量,例如:
asm volatile ("mtfsf 255,%1" : "=X" (sum) : "f" (fpenv));
sum = x + y;
在某些情况下,GCC可能会在优化时复制(或删除重复的)你的汇编代码。 如果你的asm代码定义了符号或标签,则可能导致在编译期间出现意外的重复符号错误。使用'%='
或许可以帮助解决这个问题(参看下面 Assembler Template 部分)。
6.47.2.2 Assembler Template
Assembler template 是一个包含汇编指令的文字字符串。编译器会先替换 template 中引用 inputs、outputs 和 goto labels 的标记,然后将结果字符串输出给 assembler(汇编器)。该字符串可以包含汇编器可识别的任何指令(instructions)和指示(directives)。GCC 不会自行解析这些汇编指令,也不知道它们的含义,甚至不知道它们是否是有效的汇编程序输入。 但是,它会计算语句的大小(参看Size of an asm)。
可以将多个汇编程序指令一起放在一个 asm 字符串中,并用该系统的汇编代码中通常使用的字符来分隔。在大多数地方都可以使用的字符组合是:换行符加上制表符来移至下一段汇编指令域(写作\n\t
)。有些汇编器允许分号(;
)作为行分隔符。但是,请注意,某些汇编方言使用分号做为一段注释的开始。
即使使用了 volatile 限定符,也不要期望 asm 语句序列在编译后保持完美连续。如果某些指令需要在输出中保持连续,请将它们放在单个多指令 asm 语句中。
在不使用输入/输出操作数的情况下(例如直接从 assembler template 中使用全局 C 符号)访问C程序中的数据可能无法如你预期地那样工作。 类似的,直接从 assembler template 中调用函数需要详细了解目标汇编器和ABI(应用程序二进制接口)。
由于GCC不会解析assembler template,因此它看不到它引用的任何符号。 除非这些符号也被列为输入,输出或goto操作数,否则这可能导致GCC丢弃那些未引用的符号。
- 特殊格式字符串
未完待续。。。
参考: