目录
2.4 Clobbers (破坏)和 Scratch(暂存) 寄存器
利用扩展汇编(extended asm),你可以在汇编代码中读写 C 语言变量,并从汇编代码跳转到 C 语言标签。扩展汇编语法使用冒号(‘:’)来分隔汇编模板之后的操作数参数:
asm asm-qualifiers ( AssemblerTemplate
: OutputOperands
[ : InputOperands
[ : Clobbers ] ])
asm asm-qualifiers ( AssemblerTemplate
: OutputOperands
: InputOperands
: Clobbers
: GotoLabels)
在后一种形式中,asm-qualifiers 标识符包含 goto(而在第一种形式中则不包含)。asm关键字是一个 GNU 扩展。当编写的代码需要使用 -ansi 和各种 -std 选项进行编译时,使用关键字 __asm 代替 asm 。
1. asm修饰符
1.1 volatile
扩展 asm 语句的典型用法是对输入值进行操作以生成输出值。不过,asm 语句也可能产生副作用。如果出现这种情况,你可能需要使用 volatile 限定符来禁用某些优化。
如果 GCC 的优化器判定输出变量无需使用,有时会丢弃 asm 语句。此外,如果优化器认为某段代码总是返回相同的结果(即在多次调用之间其输入值均未发生变化),则可能会将该代码移出循环。使用 volatile 限定符可以禁用这些优化。不带输出操作数的 asm 语句以及 asm goto 语句会被隐式视为 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 移出循环以生成更高效的代码。同样,使用 volatile 限定符会禁用这种类型的优化。
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 的优化器不会像处理先前示例中的非 volatile 代码那样处理这段代码。它们不会将这段代码移出循环,也不会基于“上一次调用的结果仍然有效”这一假设而将其省略。
请注意,即使是 volatile asm 指令,编译器也可能改变其相对于其他代码(包括跨越跳转指令)的位置。例如,在许多目标架构上,存在一个控制浮点运算舍入模式的系统寄存器;若使用 volatile asm 语句来设置该寄存器(如下面的 PowerPC 示例所示),往往无法保证可靠的运行结果。
asm volatile("mtfsf 255, %0" : : "f" (fpenv));
sum = x + y;
编译器可能会将加法操作移至 volatile asm 语句之前。为了确保其按预期工作,可以通过引用后续代码中的变量,为 asm 语句添加一个人为依赖,例如:
asm volatile ("mtfsf 255,%1" : "=X" (sum) : "f" (fpenv));
sum = x + y;
在某些情况下,GCC 在进行优化时可能会复制(或移除重复的)汇编代码。如果您的汇编代码中定义了符号或标签,这可能会导致编译期间出现意外的重复符号错误。使用“%=”或许有助于解决这一问题。
1.2 inline
如果使用了 inline 限定符,那么在进行内联处理时,该 asm 语句的大小会被视为可能的最小值。
某些目标平台要求 GCC 跟踪每条指令的大小,以便生成正确的代码。由于汇编语句生成的最终代码长度只有汇编器知道,GCC 必须估算其大小。它通过计算汇编模式中的指令数量,并将其乘以该处理器支持的最长指令的长度来实现这一点。(在计算指令数量时,它假设任何换行符或汇编器支持的任何语句分隔符(通常是“;”)都表示指令的结束。)
通常,GCC 的估算足以确保生成正确的代码,但如果你使用会展开成多条实际指令的伪指令或汇编宏,或者如果你使用展开后占用目标文件空间超过单条指令所需空间的汇编指令,则可能会使编译器出错。如果发生这种情况,汇编器可能会生成一条诊断信息,指出某个标签无法访问。
此大小也用于内联决策。如果你使用 asm inline 而不是仅仅使用 asm,那么为了内联的目的,asm 的大小将被视为最小大小,而忽略 GCC 认为它有多少条指令。
1.3 goto
该限定符告知编译器,asm 语句可能会跳转到 GotoLabels 中列出的某个标签。
2. 参数
2.1 AssemblerTemplate
这是一个作为汇编代码模板的字面量字符串。它由固定文本和引用输入、输出及 goto 参数的标记组合而成。
汇编模板(assembler template)是一个包含汇编指令的字符串字面量。在启用 -std=gnu++11 或更高标准的 C++ 模式下,汇编模板也可以是括号内的常量表达式(参见“C++11 常量表达式替代字符串字面量”)。
编译器会替换模板中引用输入、输出和跳转标签的标记,然后将生成的字符串输出给汇编器。该字符串可以包含汇编器能识别的任何指令,包括汇编伪指令(directives)。GCC 不会解析汇编指令本身,也不了解其含义,甚至不检查它们是否为有效的汇编输入。不过,它确实会统计其中的语句数量。
你可以将多条汇编指令放在同一个 asm 字符串中,并使用该系统汇编代码中通常使用的分隔符将它们隔开。在大多数情况下,一种有效的组合方式是使用换行符换行,并紧跟一个制表符(tab)以移动到指令字段(写作“\n\t”)。某些汇编器允许使用分号作为行分隔符。但请注意,某些汇编语言使用分号来标记注释的开始。
即使使用了 volatile 限定符,也不要指望一系列 asm 语句在编译后能保持完全连续。如果某些指令在输出中必须保持连续,请将它们放在同一个包含多条指令的 asm 语句中。
如果不使用输入/输出操作数(例如直接在汇编模板中引用全局符号)来访问 C 程序中的数据,结果可能不符合预期。同样,直接从汇编模板调用函数需要对目标汇编器和 ABI 有深入的了解。
由于 GCC 不解析汇编模板,它无法感知模板中引用的任何符号。除非这些符号同时被列为输入、输出或跳转操作数,否则 GCC 可能会因为认为它们未被引用而将其丢弃。
2.1.1 专用格式字符串
除了由 input,output 和 goto 操作数所描述的标记外,以下标记在汇编模板中也具有专有含义:
%% —— 将一个单 % 符号输进汇编代码,前一个 % 号是转义符。
%= —— 输出一个在整个编译过程中对每一个 asm 语句实例都是唯一的数。当需要在生成多条汇编指令的单个模板中创建局部标签并多次引用它们时,该选项非常有用。
%{ , %| , %} —— 在汇编代码中分别输出 { ,| 和 } 字符,前面的 % 号是转义符。若未进行转义,这些字符具有特殊含义,用于标识多种格式汇编语言(如下所述)。
2.1.2 asm 模板多汇编语言格式
对于 x86 等目标架构,GCC 支持多种汇编语言格式。-masm 选项用于指定 GCC 在处理内联汇编时默认使用的汇编方言。关于该选项的目标架构特定文档中列出了所支持的方言,以及未指定该选项时使用的默认方言。了解这些信息至关重要,因为使用某种方言编译时能正常工作的汇编代码,若改用另一种方言编译,很可能会导致失败。
如果你的代码需要支持多种汇编方言(例如,如果你正在编写需要支持各种编译选项的公共头文件),请使用如下形式的构造:
{ dialect0 | dialect1 | dialect2... }
当使用方言 #0 编译代码时,该构造输出 dialect0;使用方言 #1 时输出 dialect1,依此类推。如果花括号内的选项数量少于编译器支持的方言数量,则该构造不输出任何内容。
例如,如果某个 x86 编译器支持两种方言(“att” 和 “intel”),那么像下面这样的汇编模板:
"bt{l %[Offset],%[Base] | %[Base],%[Offset]}; jc %l2"
相当于下列字符串之一:
"btl %[Offset],%[Base] ; jc %l2" /* att dialect */
"bt %[Base],%[Offset]; jc %l2" /* intel dialect */
使用同一个编译器,下述代码
"xchg{l}\t{%%}ebx, %1"
对应下列语句之一:
"xchgl\t%%ebx, %1" /* att dialect */
"xchg\tebx, %1" /* intel dialect */
不支持嵌套方言替代项。
2.2 OutputOperands
一个以逗号分隔的列表,用于描述由 AssemblerTemplate 中的指令所修改的 C 语言变量。允许列表为空。
汇编语句可以有零个或多个输出操作数,这些操作数指示被汇编代码修改的 C 变量的名称。
在这个 i386 示例中,old(在模板字符串中用 %0 表示)和 *Base(用 %1 表示)是输出,而 Offset(用 %2 表示)是输入:
bool old;
__asm__ ("btsl %2,%1\n\t" // Turn on zero-based bit #Offset in Base.
"sbb %0,%0" // Use the CF to calculate old.
: "=r" (old), "+rm" (*Base)
: "Ir" (Offset)
: "cc");
return old;
操作数之间用逗号分隔。每一个操作数的格式如下:
[ [asmSymbolicName] ] constraint (cvariablename)
2.2.1 asmSymbolicName
指定操作数的可选符号名称。在操作数定义及汇编模板中引用该操作数时,必须在 asmSymbolicName 两侧加上方括号 [](例如 %[Value])。该名称的作用域仅限于包含其定义的 asm 语句。名称可以是任何合法的 C 语言标识符,包括在周围代码中已定义的名称。同一 asm 语句中的不同操作数不能使用相同的符号名称。
如果不使用 asmSymbolicName,则应使用操作数在汇编模板操作数列表中的位置索引(从 0 开始计数)。例如,如果有三个输出操作数,模板中应使用 %0 引用第一个,%1 引用第二个,%2 引用第三个。
2.2.2 constraint
这是一个指定操作数放置约束的字符串常量;详情请参阅“asm 操作数的约束”(Constraints for asm Operands)。在使用 -std=gnu++11 或更高标准的 C++ 代码中,该约束也可以是括号内的常量表达式。
输出约束必须以 = (表示变量将覆盖现有值)或 + (表示既读又写)开头。使用 = 时,除非操作数与输入操作数绑定,否则不要假设该位置在进入 asm 代码块时已包含现有值。
前缀之后必须跟有一个或多个附加约束(请参阅“asm 操作数的约束”),用于描述值的存储位置。常见的约束包括代表寄存器的 r 和代表内存的 m。当列出多个可能的存储位置(例如 "=rm")时,编译器会根据当前上下文选择效率最高的一个。如果尽可能多地列出 asm 语句允许的备选方案,就能让优化器生成最优代码。如果必须使用特定寄存器,但机器约束(Machine Constraints)无法提供足够的控制力来选中该寄存器,那么局部寄存器变量或许能提供解决方案(请参阅“为局部变量指定寄存器”)。
2.2.3 cvariablename
指定一个用于接收输出的 C 左值表达式,通常为变量名。外层的圆括号()是语法中必需的部分。
当编译器选择用于表示输出操作数的寄存器时,它不会使用任何被破坏(clobbered)的寄存器(参见“Clobbers 和暂存寄存器”)。
输出操作数表达式必须是左值(lvalue)。编译器无法检查操作数的数据类型对于正在执行的指令是否合理。对于不可直接寻址的输出表达式(例如位域),约束条件必须允许使用寄存器。在这种情况下,GCC 会使用该寄存器作为 asm 的输出,然后将该寄存器的值存入实际的输出位置。
使用 + 约束修饰符的操作数在计算每一个 asm 语句最多 30 个操作数的限制时,会被计为两个操作数(即既是输入又是输出)。
对于所有不得与输入操作数重叠的输出操作数,请使用 ‘&’ 约束修饰符(参见“约束修饰符字符”)。否则,GCC 可能会将输出操作数分配到与某个无关的输入操作数相同的寄存器中,因为它假设汇编代码会在产生输出之前消耗掉输入。如果汇编代码实际上包含多条指令,这一假设可能不成立。
如果一个输出参数 (a) 允许寄存器约束,而另一个输出参数 (b) 允许内存约束,也可能出现同样的问题。GCC 为访问 b 中的内存地址而生成的代码可能包含与 a 共享的寄存器,而 GCC 会将这些寄存器视为 asm 的输入。如前所述,GCC 假设这些输入寄存器会在写入任何输出之前被消耗掉。如果 asm 语句在通过 b 访问之前先写入了 a,这种假设可能会导致错误的行为。将 & 修饰符与 a 的寄存器约束结合使用,可以确保修改 a 不会影响 b 所引用的地址。否则,如果在通过 b 访问之前修改了 a,b 的位置将变得未定义。
asm 支持对操作数使用操作数修饰符(例如使用 %k2 而不仅仅是 %2)。“通用操作数修饰符”列出了所有目标架构上可用的修饰符。其他修饰符则取决于具体的硬件架构。例如,x86 架构支持的修饰符列表可在“x86 操作数修饰符”中找到。
如果 asm 之后的 C 代码没有使用任何输出操作数,请为该 asm 语句使用 volatile 关键字,以防止优化器因认为该 asm 语句多余而将其丢弃。这段代码没有使用可选的 asmSymbolicName。因此,它将第一个输出操作数引用为 %0(如果有第二个,则为 %1,依此类推)。第一个输入操作数的编号比最后一个输出操作数的编号大 1。在这个 i386 示例中,这意味着 Mask 被引用为 %1:
uint32_t Mask = 1234;
uint32_t Index;
asm ("bsfl %1, %0"
: "=r" (Index)
: "r" (Mask)
: "cc");
这段代码覆盖了变量 Index(使用 = 约束),并将值存入寄存器 r 中。使用通用的 r 约束而非特定寄存器约束,允许编译器自行选择使用的寄存器,从而生成更高效的代码;不过,如果汇编指令强制要求使用特定寄存器,则无法采用这种方式。
以下 i386 示例使用了 asmSymbolicName 语法。它产生的效果与上述代码相同,但由于在增删操作数时无需重新调整索引编号,因此有些人可能认为这种写法更具可读性或更易于维护。示例中使用的名称 aIndex 和 aMask 仅用于明确标识各名称的具体用途;实际上,直接复用 Index 和 Mask 也是完全可以的。
uint32_t Mask = 1234;
uint32_t Index;
asm ("bsfl %[aMask], %[aIndex]"
: [aIndex] "=r" (Index)
: [aMask] "r" (Mask)
: "cc");
以下是更多输出操作数的示例。
uint32_t c = 1;
uint32_t d;
uint32_t *e = &c;
asm ("mov %[e], %[d]"
: [d] "=rm" (d)
: [e] "rm" (*e));
在此,d 既可以位于寄存器中,也可以位于内存中。由于编译器可能已将 e 所指向的 uint32_t 位置的当前值存放在寄存器中,因此通过指定这两种约束,您可以让编译器为 d 选择最佳的存储位置。
2.3 InputOperands
一个以逗号分隔的列表,用于描述由 AssemblerTemplate 中的指令读取的 C 表达式。允许列表为空。
输入操作数使 C 语言变量和表达式的值可供汇编代码使用。
操作数之间用逗号分隔。每个操作数的格式如下:
[ [asmSymbolicName] ] constraint (cexpression)
2.3.1 asmSymbolicName
指定操作数的可选符号名称。在操作数定义及汇编模板中引用该操作数时,必须在 asmSymbolicName 两侧加上方括号 [](例如 %[Value])。该名称的作用域仅限于包含其定义的 asm 语句。可以使用任何合法的 C 语言标识符,包括在周围代码中已定义的名称。同一 asm 语句中的不同操作数不能使用相同的符号名称。
如果不使用 asmSymbolicName,则需使用操作数在汇编模板操作数列表中的位置索引(从 0 开始计数)。例如,若包含两个输出操作数和三个输入操作数,则模板中应使用 %2 引用第一个输入操作数,%3 引用第二个,%4 引用第三个。
2.3.2 constraint
一个字符串常量,用于指定操作数的放置约束;详情请参阅汇编操作数的约束。在 C++ 中,如果使用 -std=gnu++11 或更高版本,约束也可以是括号内的常量表达式(请参阅 C++11 常量表达式而非字符串字面量)。
输入约束字符串不能以“=”或“+”开头。当你列出多个可能的位置(例如,“irm”)时,编译器会根据当前上下文选择最有效的位置。如果你必须使用特定的寄存器,但机器约束无法提供足够的控制来选择所需的特定寄存器,则可以使用局部寄存器变量(请参阅为局部变量指定寄存器)。
输入约束也可以是数字(例如,“0”)。这表示指定的输入必须与输出约束中(从零开始)索引处的输出约束位于同一位置。当使用 asmSymbolicName 语法作为输出操作数时,你可以使用这些名称(用方括号“[]”括起来)代替数字。
2.3.3 cexpression
这是作为输入传递给汇编语句的 C 变量或表达式。括号()是语法中必需的部分。
编译器在选择用于表示输入操作数的寄存器时,不会使用任何被覆盖的寄存器(参见“覆盖寄存器和临时寄存器”)。
如果没有输出操作数但有输入操作数,则在输出操作数所在的位置放置两个连续的冒号:
__asm__ ("some instructions"
: /* No outputs. */
: "r" (Offset / 8));
2.4 Clobbers (破坏)和 Scratch(暂存) 寄存器
由 AssemblerTemplate 修改的寄存器或其他值的列表(以逗号分隔),但不包括那些已列为输出项的寄存器或值。允许列表为空。
尽管编译器知晓输出操作数列表中各项的变化,但内联汇编代码可能会修改除输出操作数以外的其他内容。例如,计算过程可能需要额外的寄存器,或者处理器执行某条汇编指令时可能会产生副作用,从而覆盖某个寄存器。为了告知编译器这些变化,需要将相关项列入“破坏列表”(clobber list)。破坏列表中的项可以是寄存器名称,也可以是特殊的破坏标识(如下文所列)。列表中的每一项都是用双引号括起来的字符串常量,各项之间用逗号分隔。在使用 -std=gnu++11 或更高标准的 C++ 代码中,破坏列表项也可以是括号内的常量表达式(参见“C++11 常量表达式而非字符串字面量”)。
破坏列表中的描述项不得与任何输入或输出操作数重叠。例如,如果某个寄存器已列入破坏列表,则不能同时定义一个描述包含该寄存器的寄存器类的操作数。若变量被声明为驻留于特定寄存器并用作 asm 的输入或输出操作数,则其涉及的寄存器不得出现在破坏列表的描述中。特别需要注意的是,无法在不将输入操作数指定为输出操作数的情况下,声明该输入操作数会被修改。
当编译器选择用于表示输入和输出操作数的寄存器时,它不会使用任何被列入破坏列表的寄存器。因此,这些标为破坏的寄存器可供汇编代码自由使用。
另一项限制是,破坏列表不得包含栈指针寄存器。这是因为编译器要求 asm 语句执行前后的栈指针值必须保持一致。然而,旧版本的 GCC 并未强制执行此规则,允许将栈指针列入列表中,但这会导致语义不明确。这种做法已弃用,在未来的 GCC 版本中,将栈指针列入破坏列表可能会导致编译错误。
以下是一个针对 VAX 架构的实际示例,展示了如何使用被破坏的寄存器:
asm volatile ("movc3 %0, %1, %2"
: /* No outputs. */
: "g" (from), "g" (to), "g" (count)
: "r0", "r1", "r2", "r3", "r4", "r5", "memory");
2.5 GotoLabels
当你使用汇编的 goto 语句时,此部分包含 AssemblerTemplate 中的代码可以跳转到的所有 C 标签列表。
汇编语句不能跳转到其他汇编语句,只能跳转到列出的 GotoLabels。GCC 的优化器不知道其他跳转,因此在决定如何优化时无法考虑这些跳转。
输入、输出和 goto 操作数的总数限制为 30 个。
asm goto 允许汇编代码跳转到一个或多个 C 语言标签。asm goto 语句中的 GotoLabels 部分包含一个以逗号分隔的列表,列出了汇编代码可能跳转到的所有 C 语言标签。GCC 默认 asm 执行完毕后会继续执行下一条语句(如果实际情况并非如此,请考虑在 asm 语句后使用 __builtin_unreachable 内建函数)。使用 hot 和 cold 标签属性可以改善 asm goto 的优化效果。
如果汇编代码修改了任何内容,请使用 memory 破坏列表项,强制优化器将所有寄存器值刷新到内存中,并在 asm 语句之后根据需要重新加载它们。
此外请注意,asm goto 语句总是被隐式视为 volatile(易变的/不可优化的)。
如果在 asm goto 内部仅针对部分可能的控制流路径设置输出操作数,则需格外小心。如果未在某条路径上设置输出且该路径上从未使用过该输出,则是允许的。否则,应使用 + 约束修饰符,表示该操作数既是输入又是输出。使用此修饰符可确保从 asm goto 出来的所有可能路径上都能获得正确的值。
若要在汇编模板中引用标签,需在标签前加上 %l(小写字母 L),后跟该标签在 GotoLabels 列表中的位置索引(从 0 开始计数)加上输入和输出操作数的总数。带有 + 约束修饰符的输出操作数会被计为两个操作数,因为它既被视为输出操作数,也被视为输入操作数。例如,如果 asm 包含三个输入操作数、一个带有 + 修饰符的输出操作数、一个带有 = 修饰符的输出操作数,并引用了两个标签,则第一个标签应引用为 %l6,第二个标签引用为 %l7。
或者,也可以使用方括号括起来的实际 C 语言标签名称来引用标签。例如,要引用名为 carry 的标签,可以使用 %l[carry]。使用此方法时,标签仍必须列在 GotoLabels 部分中。建议使用这种具名标签引用方式,因为这样可以避免计算输入和输出操作数的数量,也无需特殊处理带有 + 约束修饰符的输出操作数。
3. 评注
asm 语句允许你直接在 C 代码中嵌入汇编指令。这有助于在对时间敏感的代码中实现极致性能,或者调用 C 程序通常无法直接使用的汇编指令。
与基本 asm 类似,扩展 asm 语句既可以用于 C 函数内部,也可以用于文件作用域(即“顶层”);利用这一特性,你可以生成汇编器指令、定义可在文件其他位置调用的汇编宏,或者完全用汇编语言编写函数。位于函数外部的扩展 asm 语句不得使用任何限定符(qualifiers),不得指定破坏列表(clobbers),不得在约束(constraints)中使用 %,+ 或 & 修饰符,且仅允许使用不涉及任何寄存器的约束。
使用 naked 属性声明的函数必须使用基本 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);
4. 通用操作数修饰符
下表列出了所有目标均支持的修饰符及其作用:
| 修饰符 | 描述 | 例子 |
| c | 要求一个常量操作数,并输出不带标点符号的常量表达式。 | %c0 |
| cc | 与 %c 类似,但会尝试以不带标点符号的方式进行打印。例如,在某些架构的位置无关代码(position-independent code)中,‘%c’ 可能会无法打印常量地址。 | %cc0 |
| n | 与 ‘%c’ 类似,但在打印前会将常量的值取反 | %n0 |
| a | 将内存引用替换为实际操作数,并将该操作数视为地址。这在输出“加载地址”指令时可能很有用,因为此类指令的汇编语法通常要求将操作数写成内存引用的形式。 | %a0 |
| l | 打印标签名称,不带标点符号。 | %l0 |
2298

被折叠的 条评论
为什么被折叠?



