Linux 内联函数与内联汇编
😊
1 内联 inline
1.1 内联函数
inline
函数引入的原因:如果一些函数被频繁调用,不断地有函数栈帧的开辟和回收(即 prologue/epilogue 处理),会有非常大的开销,导致性能下降。所以引入了 inline
,将一段可能会被频繁使用的功能片段以函数的形式进行编码,然后以 inline/__inline__
关键字进行修饰(称为内联函数),在内联函数被调用的地方,该函数的代码会像宏函数一样被展开。如果程序中已经使用了 inline
关键字,建议使用 __inline__
,inline
和 __inline__
是同义的,是由 gcc 定义的宏:#define __inline__ inline
。
inline
函数使用的特点:
关键字
inline
必须与函数定义放在一起才能使函数成为内联,仅仅将inline
放在函数声明前面不起任何作用。1
2
3
4
5
6
7
8
9
10
11
12
13/* 函数 Foo 不能成为内联函数 */
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y)
{
...
}
/* 函数 Foo 是内联函数 */
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起
{
...
}如果调用一个函数的开销远小于内联函数,则不建议在函数前加
inline
。
以下情况不宜使用内联:
- 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
- 如果函数体内出现循环(
while, for
等),那么执行函数体内代码的时间要比函数调用的开销大。 inline
只适合代码简单的函数数使用,不能包含复杂的结构控制语句例如goto, switch
,并且内联函数本身不能是直接递归自己。- 如果一个函数包含静态变量。
- 如果一个函数是递归的。
- If a function return type is other than void, and the return statement doesn’t exist in a function body. 如果函数返回类型不是
void
,并且return
语句在函数主体中不存在。。
S.No. | 内联功能 | 正常功能 |
---|---|---|
1. | 当它被调用时,它会内联展开。 | 这是一个为程序提供模块化的功能。 |
2. | 它通常用于增加程序的执行时间。 | 它通常用于提高代码的可重用性,并使其更易于维护。 |
3. | 它基本上是一个函数,当函数很小并且经常被调用时使用。 | 它基本上是一组执行特定任务的语句。当功能大时使用。 |
4. | 它在其声明中需要 inline 关键字。 |
它在声明中不需要任何关键字。 |
5. | 与正常功能相比,它通常执行得更快。 | 对于小尺寸函数,它通常比内联函数执行速度慢。 |
6. | 在此中,函数主体被复制到使用它的每个上下文中,这反过来又减少了在存储设备或硬盘中搜索主体的时间。 | 在这方面,函数的主体存储在存储设备中,当每次调用该特定函数时,CPU必须将主体从硬盘加载到RAM进行执行。 |
7. | 编译器总是在编译时调用该函数的每个点放置该函数的代码副本。 | 它不提供这种类型的功能。 |
8. | 它通常只包括 2-3 行代码。 |
当行代码数量非常大时,即正常函数根据需要包含大量代码。 |
9. | 与正常功能相比,它更难理解和测试。 | 与内联功能相比,它更容易理解和测试。 |
10。 | 类中存在的函数是隐式内联的。 | 存在于类之外的函数被认为是正常函数。 |
11. | 太多的内联函数会影响编译后的文件大小,因为它复制了相同的代码。 | 太多的正常功能不会影响编译后的文件大小。 |
参考:
1.2 static inline 与 extern inline
为什么内联函数要在头文件,并且建议使用 static
?
- 内联函数放在头文件:可以让项目中使用这个内联函数的任何源文件,不必亲自再去定义一遍,直接包含这个头文件,即可像宏一样展开使用。如果是定义在
.c
源文件中,其他源文件引用时会当做extern
函数调用而不展开; - 使用
static
的原因:使用inline
定义的内联函数,编译器不一定会内联展开,那么当多个文件都包含这个内联函数的定义时,编译时就有可能报重定义错误。在头文件内联函数使用static
修饰,可以将这个函数的作用域局限在各自本地文件内,避免了重定义错误。
Linux 内核中,内联函数常常使用 static
修饰,是有原因的:
extern inline
:首先函数是extern
的(和不加该关键字效果一样,编译器默认函数是extern
),也就是说该函数可能是从其他文件引入的,如果本文件看不到定义,则只会做普通函数调用,如果能看到定义,则可以尝试inline
展开。static inline
:这是一个static
函数,也即函数是文件内链接的,内联函数的定义应该放在头文件中,即使多处包含也不会出现重定义问题(多个文件使用),如果你将它放进.c
源文件反而会出错,因为这时其他文件看不到定义,所以必须定义在头文件,然后在调用处将会做内敛展开。总结为:- 为方便项目中多文件使用
inline
函数,应该将内联函数定义在头文件中; - 为让调用内联函数代码处,能对内联函数进行展开(而不是函数调用
call
),应该使用static
进行修饰。
- 为方便项目中多文件使用
如果 inline
函数的定义放在 .c
文件中的话,如果是 static inline
修饰,其他 .c
文件不能引用该函数;如果不用 static
进行修饰(相当于 extern
),则其他 .c
文件会当做 call
调用该函数(并不是函数展开)。所以最好就是将内联函数定义在头文件中,定义成 static inline
。
参考C 内联函数:static inline 和 extern inline 的含义 、C语言:inline,static inline。
1.3 内联一定会展开吗?
inline
仅是一个对编译器的建议,并不会强制内联,所以最后能否真正内联,取决于编译器,声明内联只是对编译器的一个建议而已。
说明:在 GCC 4.8.5 20150623 (Red Hat 4.8.5-44) 版本中进行测试,仅使用 static inline
修饰函数,不开启编译优化,不会将内联函数在被调用处进行展开;而如果使用 __attribute__((always_inline))
或 __attribute__((gnu_inline))
进行修饰(强制内联),则会在函数被调用处进行展开,即使不加 static __inline__
。如果必须强制使用内联展开,则使用 __attribute__()
为主。
当一个函数使用 inline
关键字修饰,编译器在编译时一定会内联展开吗?未必。编译器也会根据实际情况,比如函数体大小、函数体内是否有循环结构、是否有指针、是否有递归、函数调用是否频繁来做决定。比如 GCC 编译器,一般是不会对内联函数展开的,只有当编译优化选项开到 -O2 以上,才会考虑是否内联展开。当我们使用 noinline
和 always_inline
对一个内联函数作了属性声明后,编译器的编译行为就变得确定了。使用 noinline
声明,就是告诉编译器,不要展开;使用 always_inline
属性声明,就是告诉编译器,要内联展开。
——《noinline & always_inline 内联函数探究》、inline只是建议内联,并不会强制内联、gcc内联函数 inline和__attribute __((always_inline))的区别。
1.4 inline 函数与宏函数区别
宏函数和内联函数有什么区别:
宏(#define) | inline 函数 | |
---|---|---|
展开时机 | 预编译阶段展开。 | 编译阶段展开,并不总是保证内联展开,视编译器优化情况而定。 |
参数传递 | 简单的做文本替换,可能会产生副作用(二义性)。宏函数只是文本替换,并不是参数传递。 | 进行参数传递。 |
参数类型安全 | 无参数类型检查;直接文本替换。如:#define add(a, b) a > b ? a:b 并不会对参数的类型进行检查,如果传递字符串则发生错误结果。 |
会对参数类型做安全检查或自动类型转换,参数和返回值的类型检查。因为内联并总是被展开,所以内联函数传参和常规函数无异。 |
用法 | 宏可用于定义常量、表达式、文字文本替换和定义函数等。 | 内联函数可用于最小化程序的执行时间。 |
调试 | 由于直接文本替换,调试可能更难。 | 更容易调试,因为它们的行为就像函数一样。 |
参考:
- 宏(#define)和内联函数(inline)的理解以及区别
- 内联函数和宏定义函数的区别
- Difference between Macro and Inline Function
- Difference between Macro and Inline Function.
- 6.7. Macros and Inline Functions
- Difference between Inline and Macro in C++
- Preprocessor Macros V.S. Inline Functions
2 内联汇编
GCC 支持在 C 代码项目中使用汇编代码,称为内联汇编(inline assembly)。使用汇编写的代码可以是一个完成的函数,被其他地方调用;也可以在一个 C 代码的函数中内嵌使用汇编代码。
在 Intel CPU 基础上,Linux 平台更喜欢使用 AT&T 风格的汇编,Windows 平台更喜欢使用 Intel 风格的语法(我是从Windows 内核转过来,更习惯 Intel 语法的汇编)。
内联汇编按格式分为两大类:基本内联汇编和扩展内联汇编。
- 基本内联汇编:常用来编写一个完全由汇编代码写成的函数(也可以嵌入到 C 函数的代码中,但是不能使用局部变量);可以使用程序中的全局变量,如
mov QWORD PTR [var_global] rax
。 - 扩展内联汇编:常嵌入在 C 函数里的汇编代码,可以使用 C 函数定义的局部变量,但要遵循复杂的语法。
2.1 基本内联汇编
基本内联汇编的使用方法如下:
1 | asm [asm_qualifiers] ("assembly code"); |
组成部分 | 说明 |
---|---|
asm | 用于声明内联汇编表达式,如果没有 [asm_qualifiers] 修饰部分的话,asm (" ") 之前有无空格是无所谓的。假如代码中已经给 asm 关键字定义为宏或做他用,则可以使用 GCC 扩展关键字 __asm__ ,两者是同义的。 GCC 定义的宏:#define __asm__ asm 。 |
[asm_qualifiers] | 可选,基本内联汇编的限定符,有 2 种: - volatile :代码编译时,编译器会对代码进行优化,达不到汇编效果,或者产生错误的结果。可以使用 volatile 告知编译器不要优化该片段,原样保留代码。可使用 GCC 定义的宏:#define __volatile__ volatile ,可避免发生冲突。基本内联汇编代码块,默认都是 volatile 的。- inline :建议编译器将该段汇编代码内联,但是不强制,编译器会自己决定。 |
(“assembly code”); | 该部分是写汇编代码的部分,括号外要有 ; 以表示结束。具体写法如下: |
("assembly code");
说明如下:
关于
" "
:可将所有汇编代码写到一对
" "
中,指令之间必须使用;
或\n
或\n\t
表示结束,最后一句汇编指令可写可不写;
。代码跨行必须要使用反斜杠\
。1
2
3
4
5
6__asm__ __volatile__ (" \
.intel_syntax noprefix; \
pushfq; \
mov QWORD PTR [rcx], rax; \
popfq; \
");可将每个汇编指令都使用
" "
括起来,每个指令必须以;
或\n
或\n\t
表示结束。代码代码跨行可不使用\
。1
2
3
4
5
6
7
8
9
10
11
12__asm__ __volatile__ (
".intel_syntax noprefix;"
"pushfq;"
"mov QWORD PTR [rcx], rax;"
"popfq;"
);
/* 或写成以下形式 */
__asm__ __volatile__ (".intel_syntax noprefix");
__asm__ __volatile__ ("pushfq");
__asm__ __volatile__ ("mov QWORD PTR [rcx], rax");
__asm__ __volatile__ ("popfq");
关于语句分隔:指令之间必须使用
;
或\n
或\n\t
(增加可读性),进行分隔。
以上使用 Intel 语法汇编,第一句汇编指令必须指定为 .intel_syntax noprefix
,同时在 GCC 编译代码时要指定 -masm=intel
参数。(GCC 默认使用 AT&T 语法,要使用 Intel 语法就需要特别指定)
6.2 扩展内联汇编
扩展内联汇编常嵌入到 C 代码中,除了可以使用函数中 C 代码定义的局部变量外,还能降低程序错误率(编译器会对某些未修正的代码或内存进行处理,但编码者不应对此充满信任)。
语法如下:
1 | asm [qualifiers] ( assembler template |
和基本内联汇编相比,扩展内联汇编在圆括号中变成了 4 部分,多了输出操作数、输入操作数和修改列表(clobber/modify,描述汇编代码中哪些寄存器、内存内修改)三项。其中的每一部分都可以省略,甚至包括 assembler template
。省略的部分要保留冒号分隔符来占位,如果省略的是最靠后的一个或多个连续的部分,分隔符也不用保留,比如省略了 clobber/modify
,不需要保留 input
后面的冒号。如下:
1 | __asm__ (" \ |
组成部分 | 说明 |
---|---|
asm | 同基本内联汇编用法。 |
[asm_qualifiers] | 扩展汇编有 3 种限定符: - volatile :同基本内联汇编用法。- inline :同基本内联汇编用法。- goto :此限定符通知编译器,汇编语句可能会执行一个跳转,跳转目的是 goto 标签里的一个。Goto Labels |
assembler template | 汇编模板,也就是写指令的地方。在基本内联汇编的基础上,扩展汇编的代码中允许存在操作数占位符,如 %0,%1等,以便对函数中 C 定义的局部变量进行使用。 值得注意的一点这是,在基本内联汇编中,寄存器表示方法和直接写汇编没什么区别,都是以 % 开头,后面跟着寄存器名称,比如 %eax ;但是在扩展内联汇编中,单个 % 有了新的用途,用来表示占位符,所以在扩展内联汇编中的寄存器名称前要用两个 % 做前缀。占位符分为序号占位符和名称占位符两种。具体参见章节 6.5。 |
2.3 修改列表
扩展汇编的最后一个冒号 :
后面表示的是修改列表。汇编代码执行后会修改一些内存或寄存器的值,通过此项通知编译器,可能造成寄存器或内存数据的破坏,这样 GCC 就知道哪些寄存器或内存需要提前保存起来。
每个被修改过的寄存器名称(不包括堆栈寄存器 rbp, rsp
)或内存使用双引号括 " "
起来,多个的话使用 ,
分割双引号。主要分为三种:
- 通用寄存器:将寄存器名称写在双引号中,
"%rcx", "%rbx"
等; - 标志寄存器:使用
"cc"
,表示标志寄存器rflags
中的某些位被修改(在使用asm
前后); - 内存:使用
"memory"
,表示在使用asm
后内存有被修改。
如:
1 | __asm__ (" \ |
2.4 输入输出操作数
输出操作数:在汇编代码片段中,将数据输出到 C 定义的局部变量中(类似于 Windows 中的 OUT
),以便给 C 代码使用。
输入操作数:可以将函数中 C 代码定义的局部变量或函数的参数,输入给汇编使用。定义格式与输出操作数一样。
定义格式如下:
1 | [变量别名]"[操作数修饰符] 约束条件"(C变量名) |
双引号和括号都不能忽略,如果是有多个输出操作数,使用 ,
分割:"=r"(a), "=o"(b)
。
- 操作数修饰符(modifier):可选的,只能用于输出,提供输出变量的行为信息(如只读/读写、是否可以在指令中交换次序等)。
- 约束条件(constraint):提供输入/输出操作数位置的信息(如分配到寄存器还是内存中)。
- 变量别名(asmSymboicName):可选的,我们可以给
C变量名
起一个只能在该 asm 中使用的别名,并通过%[asmSymbolicName]
访问它。比如:[value] "=m" (i)
,可以在 asm 块中通过%[value]
访问它。
一、操作数修饰符(modifier)
操作数修饰符(modifier)只能用于输出变量上。
=
:操作数是只写的。这意味着操作对象中原先的值可以被丢弃,并且写入新数据。+
:操作数是可读写的。即可以在 asm 中合法的读取操作对象,并且 C 变量在进入 asm 块时就已经加载到对应的位置中。%
:表示该操作对象可以交换次序。该操作数可以和下一个输入操作数互换。&
:表示此输出操作数要独占所约束(分配)的寄存器,任何输入操作数中所分配的寄存器不能与此相同。注意,当表达式中有多个修饰符时,&
要与约束名挨着,不能分隔。
其他注意事项:
- GCC不保证在进入 asm 块时,输出变量已被加载到约束条件(constraint)指定的位置中。如果需要 GCC 在进入 asm 块时将变量加载到约束条件(constraint)指定的位置中,则操作数修饰符(modifier)应该使用
+
。 - 约束条件(constraint)是指定变量在 asm 块中的位置,而不是在函数中的位置。如变量的约束条件(constraint)为
r
说明它在进入/退出 asm 块时被分配到通用寄存器中,但在进入 asm 块前它的位置是不确定的。如果要控制 asm 块外变量被分配的位置,可以使用 GNU C 的寄存器变量拓展。
二、约束条件(constraint)
约束条件:描述了操作数是否使用了寄存器,用的是哪种寄存器;是否引用了内存,用的是哪种地址;是否是立即数,是哪种类型的立即数等等。它所起的作用就是把 C 代码中的操作数(变量、立即数)映射为汇编中所使用的操作数,实际就是描述 C 中的操作数如何变成汇编操作数。分为以下几种:
- 寄存器约束
- 内存约束
- 立即数约束
- 数字约束
约束类型 | 描述 |
---|---|
寄存器约束 | 在使用汇编代码片段时(不包含 C 代码),将操作数存储在寄存器中。 常见的寄存器约束有: r:表示 rax/rbx/rcx/rdx/rsi/rdi 这 6 个通用寄存器中任意一个 a:表示 a 系列寄存器 rax/eax/ax/al b:表示 b 系列寄存器 rbx/ebx/bx/bl c:表示 c 系列寄存器 rcx/ecx/cx/cl d:表示 d 系列寄存器 rdx/edx/dx/dl D:表示寄存器 rdi/edi/di S:表示寄存器 rsi/esi/si q:表示 rax/rbx/rcx/rdx 这 4 个通用寄存器中任意一个 g:表示可以存放到任意地点(寄存器和内存)。相当于除了同 q 一样外,还可以让 gcc 安排在内存中 A:a 和 d 寄存器,用于返回结果保存在a 和 d 寄存器的指令。 f:表示浮点寄存器 t:表示第 1 个浮点寄存器 u:表示第 2 个浮点寄存器 p:表示内存地址,给 “load address” 和 “push address” 指令使用 注意:我们在写 AT&T 汇编代码时,想使用 movb, movd, movq 等操作指定字节大小的操作数时,代码编译会发生错误,还需要对占位符进一步精确使用,具体见章节 6.5 占位符部分。 |
内存约束 | 表示在汇编代码中直接操作 C 变量的内存地址(操作的是变量地址),不需要寄存器做中转,直接进行内存读写,也就是汇编代码的操作数是 C 变量的指针。主要有以下几种:m :表示内存操作数,可以是计算机支持的任何类型的地址。o :也是内存操作数,但访问它是通过偏移量的形式访问,即包含 offset_address 的格式。如 asm ("movq 16+%1, %0" : "=r"(element) : "o"(arr)); 将变量 1 地址偏移 16 的数值写入到变量 0。V : 一个不允许偏移的内存操作数。换言之,任何适合 m 约束而不适合 o 约束的操作数。 |
立即数约束 | 常数。立即数不是变量,只能作为右值,只用于输入操作数,此约束要求 gcc 在传值的时候不通过寄存器或内存,直接作为立即数传给汇编代码 i:表示操作数为整数立即数 F:表示操作数为浮点数立即数 I(大写的i):表示操作数为0~31之间的立即数 J:表示操作数为0~63之间的立即数 N:表示操作数为0~255之间的立即数 O:表示操作数为0~32之间的立即数 X:表示操作数为任何类型立即数 |
数字约束 | 0~9 :此约束只用于输入操作数,表示该输入 操作数与输出操作数中第 n 个操作数用相同的寄存器或内存。如 asm ("incl %0" :"=a"(var):"0"(var)); 。这里的 0 用于指定与第 0 个输出变量相同的约束。 |
x86 特有的约束 | r:寄存器操作数约束,查看上面给定的表格 q:寄存器 a、b、c 或者 d I:范围从 0 到 31 的常量(对于 32 位移位) J:范围从 0 到 63 的常量(对于 64 位移位) K:0xff L:0xffff M:0、1、2 或 3 (lea 指令的移位) N:范围从 0 到 255 的常量(对于 out 指令) f:浮点寄存器 t:第一个(栈顶)浮点寄存器 u:第二个浮点寄存器 A:指定 “a” 或 “d” 寄存器。这主要用于想要返回 64 位整形数,使用 “d” 寄存器保存最高有效位和 “a” 寄存器保存最低有效位。 |
2.5 汇编模板中的占位符
章节 6.2 和 6.4 中提到过,汇编模板(assembler template)中使用 2 种占位符:序号占位符、名称占位符。
序号占位符:
%0~%9
,按照操作数在输出操作数列表 --> 到输入操作数列表从左到右出现的次序,从 0 开始编号,一直到 9,引用它的格式是%0~%9
,也就是说最多支持 10 个序号占位符。如下,输出操作数列表出现的第一个操作数为
b
变量,汇编代码中的%0
即表示引用该变量。同理,出现的第二个操作数是
a
变量,汇编代码中的%1
即表示引用该变量。1
2
3
4
5
6
7
8
9
10
11
12
void main() {
int a = 10, b;
asm ("movl %1, %%eax;"
"movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
printf("Now, b is: %d\n", b);
}名称占位符:输出操作数的格式为:
1
[变量别名]"[操作数修饰符] 约束条件"(C变量名)
其中可选的变量别名(asmSymboicName):我们可以给
C变量名
起一个只能在该 asm 中使用的别名,并通过%[asmSymbolicName]
访问它。比如:[value] "=m" (i)
,可以在 asm 块中通过%[value]
访问它。
关于章节 6.4 中提到的,在汇编模板(assembler template)中写 AT&T 汇编代码时,想使用 movb, movd, movq
等操作指定字节大小的操作数时,代码编译会发生错误,如下:
1 |
|
GCC 编译时会产生错误,提示 mov
指令的后缀没用对,正常情况下不应高提示错误。因为 a, b
变量是 64 bits,代码只取低 32 bits。
1 | [alvin@centos-7 program]$ gcc -g -w -fPIC -o 1225_exp_asm.exe 1225_exp_asm.c |
还需要对占位符进一步精确使用:所以应该使用更精确的占位符。如下表所示,取 %rax
的低 32 bits,应该是使用 %kn
占位符。
The table below shows the list of supported modifiers and their effects.
Modifier | Description | Operand | ‘att’语法 | ‘intel’语法 |
---|---|---|---|---|
A |
Print an absolute memory reference. | %A0 |
*%rax |
rax |
b |
Print the QImode name of the register. | %b0 |
%al |
al |
B |
print the opcode suffix of b. | %B0 |
b |
|
c |
Require a constant operand and print the constant expression with no punctuation. | %c1 |
2 |
2 |
d |
print duplicated register operand for AVX instruction. | %d5 |
%xmm0, %xmm0 |
xmm0, xmm0 |
E |
Print the address in Double Integer (DImode) mode (8 bytes) when the target is 64-bit. Otherwise mode is unspecified (VOIDmode). | %E1 |
%(rax) |
[rax] |
g |
Print the V16SFmode name of the register. | %g0 |
%zmm0 |
zmm0 |
h |
Print the QImode name for a “high” register. | %h0 |
%ah |
ah |
H |
Add 8 bytes to an offsettable memory reference. Useful when accessing the high 8 bytes of SSE values. For a memref in (%rax), it generates | %H0 |
8(%rax) |
8[rax] |
k |
Print the SImode name of the register. | %k0 |
%eax |
eax |
l |
Print the label name with no punctuation. | %l3 |
.L3 |
.L3 |
L |
print the opcode suffix of l. | %L0 |
l |
|
N |
print maskz. | %N7 |
{z} |
{z} |
p |
Print raw symbol name (without syntax-specific prefixes). | %p2 |
42 |
42 |
P |
If used for a function, print the PLT suffix and generate PIC code. For example, emit foo@PLT instead of ’foo’ for the function foo(). If used for a constant, drop all syntax-specific prefixes and issue the bare constant. See p above. |
|||
q |
Print the DImode name of the register. | %q0 |
%rax |
rax |
Q |
print the opcode suffix of q. | %Q0 |
q |
|
R |
print embedded rounding and sae. | %R4 |
{rn-sae}, |
, {rn-sae} |
r |
print only sae. | %r4 |
{sae}, |
, {sae} |
s |
print a shift double count, followed by the assemblers argument delimiterprint the opcode suffix of s. | %s1 |
$2, |
2, |
S |
print the opcode suffix of s. | %S0 |
s |
|
t |
print the V8SFmode name of the register. | %t5 |
%ymm0 |
ymm0 |
T |
print the opcode suffix of t. | %T0 |
t |
|
V |
print naked full integer register name without %. | %V0 |
eax |
eax |
w |
Print the HImode name of the register. | %w0 |
%ax |
ax |
W |
print the opcode suffix of w. | %W0 |
w |
|
x |
print the V4SFmode name of the register. | %x5 |
%xmm0 |
xmm0 |
y |
print “st(0)” instead of “st” as a register. | %y6 |
%st(0) |
st(0) |
z |
Print the opcode suffix for the size of the current integer operand (one of b /w /l /q ). |
%z0 |
l |
|
Z |
Like z , with special suffixes for x87 instructions. |
如上的示例改成如下:
1 |
|
参考内容: