关于本文档
用于 ARM RISC 处理器的 GNU C 编译器提供了将汇编语言代码嵌入到 C 程序中。这个很酷的特性可以用来手动优化软件的关键部分或者使用 C 语言中无法使用的处理器指令。
这里假设您熟悉编写 ARM 汇编程序,因为这不是一个 ARM 汇编程序编程教程。 它也不是C语言教程。
所有示例都在 GCC version 4 上测试通过,但是大多数也能在更早的版本上运行。
GCC asm 语句
我们先从一个简单的示例开始。 下面的语句可以像其他C语句一样包含在代码中。
/* NOP example */
asm("mov r0, r0");
这段代码将 r0 寄存器的内容移动到 r0 寄存器。换句话说,就是啥都没做。这也被称作 NOP (无操作)语句,通常被用于非常短的延迟。
慢着!在把这个示例立即加到你的 C 代码前,继续阅读和学习,为什么它并不能如预期那样工作。
在内联汇编中,你可以使用与编写纯ARM汇编代码相同的汇编指令助记符。而且你可以在单行内联 asm 语句中编写多个汇编指令。为了使其更具可读性,你可以将每个指令放在单独的一行。
asm(
"mov r0, r0\n\t"
"mov r0, r0\n\t"
"mov r0, r0\n\t"
"mov r0, r0"
);
换行符和制表符的特殊序列将使汇编程序清单看起来很漂亮。 乍一看可能有点奇怪,但这就是编译器在编译C语句时创建自己的汇编代码的方式。
至此,汇编指令与纯汇编语言程序中出现的指令基本相同。 但是,如果寄存器和常量引用的是C表达式,则用不同的方式指定它们。 内联 asm 语句的一般形式是
asm(
code
: output operand list
: input operand list
: clobber list
);
汇编语言与 C 操作符之间的连接是由asm语句中可选的第二和第三部分提供的,即输出和输入操作数列表。我们会稍后再解释第三部分,clobbers列表。
下一个例子,旋转比特位,将 C 变量传递给汇编语言。它取一个整型变量的值,向右旋转 1 bit 并将结果储存在第二个整型变量中。
/* Rotating bits example */
asm("mov %[result], %[value], ror #1": [result] "=r" (y): [value] "r" (x));
每一条 asm 语句被冒号切分为四个部分:
1. 汇编指令,定义在一条字符串中:
"mov %[result], %[value], ror #1"
2. 可选的输出操作数列表,使用逗号分隔。每个条目包含一个用方括号括起来的符号名称,后跟一个约束字符串,再后跟一个用括号括起来的C表达式。我们的示例中只有一条:
[result] "=r" (y)
3. 使用逗号分隔的输入操作数列表,其语法与输入操作数列表一致。同样的,这也是可选的,我们示例中只有一个操作数:
[value] "r" (x)
4. 可选的修饰寄存器列表,我们示例中省略了。
如初始NOP示例所示,asm语句的末尾部分如果未使用,可以省略。仅包含汇编程序指令的内联asm语句也称为基本内联程序集,而包含可选部件的语句称为扩展内联程序集。如果一个未使用的部分后面跟着一个需要用上的部分,那么必须将其留空。下面的示例设置 ARM CPU 的当前程序的状态寄存器。它使用一个输入,但是没有输出寄存器。
asm("msr cpsr,%[ps]" : : [ps]"r"(status));
假如代码部分为空,则需要一个空字符串。下面的语句创建了一个特殊的 clobber 来告诉编译器,内存内容可能已经被修改。同样,clobber 列表将在稍后的代码优化中解释。
asm("":::"memory");
也可以插入空格、换行,甚至 C 风格注释来增加可读性。
asm("mov %[result], %[value], ror #1"
: [result]"=r" (y) /* Rotation result. */
: [value]"r" (x) /* Rotation value. */
: /* No clobbers */
);
在代码部分中,使用百分号跟上方括号括起来的相关符号名称表示。它指向一个操作数列表中包含同样符号名称的项。在 Rotating Bits 的示例中:
- %[result] 表示输出操作数,C 变量 y
- %[value] 表示输入操作数,C 变量 x
操作数的符号名称使用独立的命名空间。也就是说,与其他符号表都没有关联。简单来讲,你可以不用关心所选择的名字是否与你的 C 代码中已有的名字重复。但是,在每一个 asm 语句中,符号名必须唯一。
如果您已经看过其他作者编写的一些可工作的内联汇编语句,那么您可能会注意到一个显著的差异。 事实上,GCC 编译器从 3.1 版本才开始支持符号化的命名。在早期版本中,Ratating Bits 示例必须写成这样:
asm("mov %0, %1, ror #1" : "=r" (result) : "r" (value));
操作数由百分号加一个数字表示,%0表示第一个操作数,%1表示第二个操作数,以此类推。这种形式在最新的 GCC 版本中依然支持,但是相当容易出错且难以维护。想象一下,当你已经编写好一大堆汇编指令,如果要插入一个新的输出操作数,则需要手动将其后的所有操作数都重新编号。
如果所有这些看起来依然有点奇怪,别担心。除开神秘的 clobber 列表,你一定察觉到还少了些什么,对吧?事实上,我们还没有讲操作数列表中的限制字符串。我想请你耐心些。下一章还有一些更加重要的内容要强调。
C 代码优化
使用汇编语言可能有两个原因。第一个,当我们想要更贴近硬件时,C 语言是有限制。比如,没有 C 语句能直接修改处理器的状态寄存器。第二个原因则是构建高度优化的代码。不用怀疑,GNU 的 C 代码优化器(optimizer)很不错,但是与手工编写的汇编代码相比还是差远了。
这一章的主题常常被忽略:当使用内联汇编语句添加汇编语言代码时,这些代码也是用 C 编译器的代码优化器处理的。让我们看看编译器使用我们的 Rotating Bits 示例生成的清单部分(a part of compiler listing):
00309DE5 ldr r3, [sp, #0] @ x, x
E330A0E1 mov r3, r3, ror #1 @ tmp, x
04308DE5 str r3, [sp, #4] @ tmp, y
编译器选择了 r3 寄存器来做位旋转。它也可以选择任何其他寄存器,或者两个寄存器,每个 C 变量一个。也可能不会显示的加载数值或者存储结果。这里还有另一个清单,由不同的编译选项和不同的编译器版本生成:
E420A0E1 mov r2, r4, ror #1 @ y, x
这个编译器为每个操作数选择了一个唯一的寄存器,使用 r4 中已经缓存的数值,然后将结果传递到 r2。明白了吗?
通常情况会变得更糟。编译器甚至可能决定不包含你的汇编代码。这些决定是编译器优化策略的一部分,并且取决于使用你的汇编指令的上下文。举个例子,如果你从未在 C 程序的其余部分使用任何输出操作数,优化器很可能会删除你的内联汇编语句。我们最初提供的 NOP 示例也可能是这样一个例子,因为对编译器来说,这是无用的开销,会减慢程序的执行。
解决方法是给 asm 语句添加 volatile 属性,来告诉编译器将你的汇编代码从代码优化中排除。记住,我们已经警告你别使用最初的例子。下面是改进后的版本:
/* NOP example, revised */
asm volatile("mov r0, r0");
但是,还有更麻烦的事情在等着我们。一个成熟的(sophisticated)优化器会重新排列代码。 下面的C代码在最后几分钟的修改中被遗漏:
i++;
if (j == 1)
x += 3;
i++;
优化器可以识别出两次自增不会对条件语句有任何影响。此外,它还知道,将一个数值增加 2 只会使用一条 ARM 指令。因此,它将代码重排成下面这样:
if (j == 1)
x += 3;
i += 2;
以便节省一条 ARM 指令。结果就是:不能保证编译后的代码会保留原始代码中的语句顺序。
这可能会极大的影响你的代码,正如我们将要演示的。下面的代码打算将 c 与 b 相乘,其中一个或者两个都可能会被中断程序修改。在访问变量前失能中断,然后再使能看起来是个好主意。
asm volatile(
"mrs s12, cpsr\n\t"
"orr r12, r12, #0xC0\n\t"
"msr cpsr_c, r12\n\t"
:
:
: "r12", "cc");
c *= b;
asm volatile(
"mrs r12, cpsr\n"
"bic r12, r12, #0xC0\n"
"msr cpsr_c, r12"
:
:
: "r12", "cc");
不幸的是,优化器可能决定先做乘法然后再执行两个内联汇编指令或者反过来。这使得我们的汇编代码毫无用处。
我们可以借助 clobber 列表来解决这个问题。上面例子中的 clobber 列表是
"r12", "cc"
这通知编译器,汇编代码修改了寄存器 r12 并且更新了条件代码的标志位。顺便讲一句,使用硬编码寄存器通常会阻止获得最佳的优化结果。一般来说,你应当传递一个变量,然后让编译器来选择合适的寄存器。除开寄存器名字和 "cc" 代表条件寄存器外,"memory" 也是一个有效的关键字。它告诉编译器该汇编指令可能改变了内存位置。这将强迫编译器在执行汇编指令前保存 cache 内容,并在执行完后重新加载它们。并且必须保持顺序执行,因为在使用 memory clobber 的汇编语句执行后,所有变量的内容都是不可预测的。
asm volatile(
"mrs s12, cpsr\n\t"
"orr r12, r12, #0xC0\n\t"
"msr cpsr_c, r12\n\t"
:
:
: "r12", "cc", "memory");
c *= b; /* This is safe. */
asm volatile(
"mrs r12, cpsr\n"
"bic r12, r12, #0xC0\n"
"msr cpsr_c, r12"
:
:
: "r12", "cc", "memory");
使所有缓存失效可能不是好的选择。或者你也可以添加一个 dummy 操作数来创造一个人为的依赖:
asm volatile(
"mrs s12, cpsr\n\t"
"orr r12, r12, #0xC0\n\t"
"msr cpsr_c, r12\n\t"
: "=X" (b)
:
: "r12", "cc");
c *= b; /* This is safe. */
asm volatile(
"mrs r12, cpsr\n"
"bic r12, r12, #0xC0\n"
"msr cpsr_c, r12"
:
: "X" (c)
: "r12", "cc");
这段代码在第一段 asm 语句中假装要修改变量 b,然后在第二段中假装要使用变量 c 。这将保留三条语句的顺序,而不会失效其他缓存的变量。
理解优化器如何影响内联汇编语句是至关重要的。 如果有些东西仍然含糊不清,在继续下一个话题之前,最好重新阅读这一部分。
输入和输出操作数
我们知道,每个输入和输出操作数都是用方括号括起来的符号名称来描述的,后面跟着一个约束字符串,约束字符串后面跟着一个用圆括号括起来的C表达式。
这些约束是什么?我们又为什么需要它们呢?你可能知道,每个汇编指令只接受特定的操作数类型。比如, branch 指令期望一个目的地址来跳转。但是,并不是每个内存地址都是有效的,因为最终的操作码只接受一个 24bit的偏移。相反,branch 和 exchange 指令需要一个容有32bit目标地址的寄存器。在这两种情况下,从 C 传递给内联汇编的操作数可能是同一个 C 函数指针。因此,当传递常量、指针或者变量到内联汇编语句时,内联汇编程序必须知道它们在汇编代码中如何表示。
对于 ARM 处理器,GCC 4 提供了以下约束。
Constraint | Usage in ARM state | Usage in Thumb state |
f | Floating point resiter f0 .. f7 | Not available |
h | Not available | Registers r8..r15 |
G | Immediate floating point constant | Not available |
H | Same a G, but negated | Not available |
I | Immediate value in data processing instructions | Constant in the range 0 .. 255 |
J | Indexing constants -4095 .. 4095 | Constant in the range -255 .. -1 |
K | Same as I, but inverted | Same as I, but shifted |
L | Same as I, but negated | Constant in the range -7 .. 7 |
l | Same as r | Registers r0..r7 |
M | Constant in the range of 0 .. 32 or a power of 2 | Constant that is a multiple of 4 in the range of 0 .. 1020 |
m | Any valid memory address | |
N | Not available | Constant in the range of 0 .. 31 |
O | Not available | Constant that is a multiple of 4 in the range of -508 .. 508 |
r | General register r0 .. r15 | Not available |
w | Vector floating point registers s0 .. s31 | Not available |
X | Any operand | Any operand |
约束字符可以由单个约束修饰符作为前缀。不带修饰符的约束代表只读操作数。修饰符如下:
Modifier | Specifies |
= | Write-only operand, usually used for all output operands |
+ | Read-write operand, must be listed as an output operand |
& | A register that should be used for output only |
输出操作数必须是只写的并且 C 表达式 result 必须是一个左值,也就是说该操作数必须可以出现在赋值语句的左边。C 编译器能够检查这个。
输入操作数是只读的。注意,C 编译器不能检查对于汇编指令使用的操作,操作数的类型是否合理。大多数问题会在后面的组装(assmebly)阶段检测到,这一阶段以其怪异的错误消息闻名。即使它报告说发现了一个应当理解上报给作者(指编译器的作者)的内部编译问题,你也应当首先检查你的内联汇编代码。
一条铁律:永远不要对输入操作数进行写操作。但是,如果输入输出需要同一个操作数呢?约束修饰符 + 的作用如下所示:
asm("mov %[value], %[value], ror #1" : [value] "+r" (y));
这与我们上面的 Ratating Bits 示例很像。它向旋转变量 value 的内容1个bit。与上面例子相反的是,结果并没有保存到另一个变量里。输入变量的原始内容将被修改。
修饰符 + 可能不被早期编译器版本支持。幸运的是他们提供另一种解决方法,在最新的编译器版本上依然支持。对于输入操作数,可能在约束字符串中使用一个单独的数字。使用数字n告诉编译器使用与第n个操作数相同的寄存器,从0开始。下面是例子:
asm("mov %0, %0, ror #1" : "=r" (value) : "0" (value));
约束"0"告诉编译器,使用第一个输出操作数所用的寄存器作为输入寄存器。
无论如何都要注意的是,这并不意味着可以反过来用。编译器可以为输入和输出选择同一个寄存器,即使没有被告知需要如此。你可能还记得前面前面两个变量的 Rotating Bits 示例的汇编 listing,编译器为两个变量使用了同一个 r3 寄存器。asm 语句如下:
asm("mov %[result],%[value],ror #1":[result] "=r" (y):[value] "r" (x));
生成了如下代码:
00309DE5 ldr r3, [sp, #0] @ x, x
E330A0E1 mov r3, r3, ror #1 @ tmp, x
04308DE5 str r3, [sp, #4] @ tmp, y
大多数情况下这都不会有问题,但是如果输出操作数在输入操作数被使用前就被汇编代码修改,则可能发生致命错误。在代码依赖于输入和输出操作数使用不同的寄存器的情况下,你必须在输出操作数上添加 & 约束修饰符。下面的代码展示了这个问题。
asm volatile("ldr %0, [%1]" "\n\t"
"str %2, [%1, #4]" "\n\t"
: "=&r" (rdv) // %0 output
: "r" (&table), "r" (wdv) // %1-table, %2-wdv, input
: "memory");
从一个表中读取一个值,然后将另一个值写入到这个表的另一个位置。如果编译器为输入和输出操作数选择了同一个寄存器,那么输出值将在第一个汇编指令上被摧毁(destroyed)。幸运的是,& 修饰符指示编译器不要为输出值选择任何被输入操作数使用的寄存器。
更多诀窍
内联汇编作为预处理宏
为了复用你的汇编语言部分的代码,将它们定义为宏,并放在头文件中非常有用。如果在严格的 ANSI 模式下编译的模块中使用这种头文件可能产生编译器警告。为了避免这种情况,你可以使用 __asm__ 替代 asm,__volatile__替代 volatile。这些都是等价的别名。下面是一个宏,将一个 long 型从小端转到大端,反之亦然:
#define BYTESWAP(val) \
__asm__ __volatile__ ( \
"eor r3, %1, %1, ror #16\n\t" \
"bic r3, r3, #0x00FF0000\n\t" \
"mov %0, %1, ror #8\n\t" \
"eor %0, %0, r3, lsr #8" \
: "=r" (val) \
: "0"(val) \
: "r3", "cc" \
);
C stub 函数
宏定义在被引用时会包含同样的汇编代码。这在大(large)的工程中可能不被接受。这种情况下,你可以定义一个 C stub 函数。下面同样是字节序交换,这次使用 C 函数实现:
unsigned long ByteSwap(unsigned long val)
{
asm volatile (
"eor r3, %1, %1, ror #16\n\t"
"bic r3, r3, #0x00FF0000\n\t"
"mov %0, %1, ror #8\n\t"
"eor %0, %0, r3, lsr #8"
: "=r" (val)
: "0"(val)
: "r3"
);
return val;
}
替换 C 变量的符号名
默认 GCC 在 C 和汇编代码中使用相同的函数名或者变量名。你可以通过使用一个特殊的 asm 语句,为汇编代码指定一个不同的名字:
unsigned long value asm("clock") = 3686400;
这条语句指示编译器使用符号名 clock 而不是 value。这只对全局变量生效。局部变量在汇编代码中没有符号名称。
替换 C 函数的符号名
要修改函数的符号名,你需要一个原型声明,因为编译器在函数定义时不接受 asm 关键字:
extern long Calc(void) asm ("CALCULATE");
调用函数 Calc() 会创建汇编指令去调用函数 CALCULATE。
强制使用特定寄存器
局部变量都被保存在一个寄存器中。你可以指示内联汇编为其使用特定的寄存器。
void Count(void) {
register unsigned char counter asm("r3");
... some code...
asm volatile("eor r3, r3, r3" : "=l" (counter));
... more code...
}
汇编指令 "eor r3, r3, r3" 会清除变量 counter。请注意,这个示例在大多数情况下都是不好的,因为它干扰了编译器的优化器。 此外,GCC不会完全保留指定的寄存器。如果优化器识别到该变量不再被引用,这个寄存器可能会被重用(re-used)。但是编译器没有能力检查这个寄存器的使用是否与任何预先定义的寄存器冲突。如果你使用这种方式保留了太多寄存器,编译器可能在代码生成阶段耗尽寄存器。
临时使用寄存器
如果你要使用没有被作为操作数传入的寄存器,你需要告知编译器。下面的代码将一个值调整为 4 的倍数。它使用r3作为临时寄存器,并通过在clobber列表中指定r3来让编译器知道这一点。 此外 CPU 状态标志被 ands 指令修改,因此 cc 被添加到 clobbers。
asm volatile(
"ands r3, %1, #3" "\n\t"
"eor %0, %0, r3" "\n\t"
"addne %0, #4"
: "=r" (len)
: "0" (len)
: "cc", "r3"
);
同样,硬编码寄存器的使用总是糟糕的编码风格。 最好实现一个C stub 函数,并使用局部变量作为临时值。
使用常数
可以使用 mov 指令加载一个立即常数到一个寄存器。基本上,这个值限制在0到255之间。
asm("mov r0, %[flag]" : : [flag] "I" (0x80));
但当以偶数位旋转给定范围时,也可以使用更大的值。 换句话说,任何结果为的数,其中 n 的范围是[0,255],X 是一个[0,24]范围内的偶数。因为是旋转,X 也可以是 26,28 或者 30,这样 37 到 32 位被折叠到 5 到 0 。最后,当使用mvn代替mov时,可以给出这些值的二进制补码。
有时也需要跳转到一个固定的内存地址,该地址可能由预处理宏定义。可以使用如下汇编代码:
ldr r3, =JMPADDR
bx r3
这适用于任何合法的数值。如果常量合适(比如 0x20000000),聪明的汇编器会将其转换为:
mov r3, #0x20000000
bx r3
如果不合适(比如 0x00F000F0),汇编器会从文字池(literal pool,根据下面代码猜测为符号表中的文字解析)中加载数值。
ldr r3, .L1
bx r3
...
.L1: .word 0x00F000F0
对于内联汇编,也同样的用法。但不是使用 ldr,你可以简单地提供一个常量作为寄存器值:
asm volatile("bx %0" : : "r" (JMPADDR));
取决于常数的实际数值,mov、ldr 或者其他变体都可以使用。如果 JMPADDR 被定义为 0XFFFFFF00,那么最终的代码可能类似于
mvn r3, #0xFF
bx r3
现实世界要复杂得多。 我们可能需要加载一个常量到特定的寄存器。假设我们想调用一个子程序,但我们想返回到另一个地址,而不是跟随分支(follows our branch)的地址。 当嵌入式固件从 main 返回时,这是很有用的。 在这种情况下,我们需要加载链接寄存器。下面是汇编代码:
ldr lr, =JMPADDR
ldr r3, main
bx r3
你知道如何在内联汇编中实现这个吗? 这里有一个解决方案:
asm volatile(
"mov lr, %1\n\t"
"bx %0\n\t"
: : "r" (main), "I" (JMPADDR));
但是这样依然有一个问题。我们在这里使用了 mov 指令,只有 JMPADDR 合适才能工作。结果代码将与我们在纯汇编代码中得到的代码相同。 如果它不合适,那么我们需要使用 ldr 替代。但不幸的是,没有办法在内联汇编中表示
ldr lr, =JMPADDR
反而,我们必须写成
asm volatile(
"mov lr, %1\n\t"
"bx %0\n\t"
: : "r" (main), "r" (JMPADDR));
与纯汇编代码相比,我们使用了额外的语句作为结束,使用了一个额外的寄存器。
ldr r3, .L1
ldr r2, .L2
mov lr, r2
bx r3
寄存器用法
分析C编译器的汇编清单输出并研究生成的代码总是一个好主意。 下表是编译器的典型寄存器用法,这可能有助于理解代码。
Register | Alt. Name | Usage |
r0 | a1 | First function argument |
r1 | a2 | Second function argument |
r2 | a3 | Third function argument |
r3 | a4 | Fourth function argument |
r4 | v1 | Register variable |
r5 | v2 | Register variable |
r6 | v3 | Register variable |
r7 | v4 | Register variable |
r8 | v5 | Register variable |
r9 | v6 | Register variable |
r10 | sl | Stack limit |
r11 | fp | Argument pointer |
r12 | ip | Temporary workspace |
r13 | sp | Stack pointer |
r14 | lr | Link register |
r15 | pc | Program counter |
常见的陷阱
指令顺序
开发者经常期望最终代码里的指令顺序与源代码中的一致。这个假设是错误的,并且经常引入难以发现的 bug。事实上,asm 语句与其他 C 语言代码一样被优化器处理。如果依赖关系允许的,他们会被重新排序。
“C 代码优化”一章讨论了细节也提供了解决方案。
定义一个变量作为指定寄存器
即使一个变量被强制指定给一个指定的寄存器,最终代码也可能会不如预期的工作。考虑如下代码:
int foo(int n1, int n2) {
register int n3 asm("r7") = n2;
asm("mov r7, #4");
return n3;
}
编译器被告知使用 r7 作为局部变量 n3,n3 由参数 n2 初始化(赋值)。然后内联汇编语句将 r7 设置为 4,最后应该返回该值。然而,这可能完全错了。 记住,编译器不能识别内联汇编中发生了什么。 但是优化器在 C 代码上很聪明,生成以下汇编代码。
foo:
mov r7, #4
mov r0, r1
bx lr
不是返回r7,而是返回n2的值,它通过 r1 传递给我们的函数。发生了什么?好吧,C 代码优化器决定 n3 不是必须的,同时最终的代码依然包含我们的内联汇编语句。它直接返回了参数 n2 。
只将一个变量指派给一个固定寄存器并不代表 C 编译器会使用这个变量。我们依然要告诉编译器,这个变量在内联汇编操作中被更改。对于上面给出的例子,我们需要一个输出操作数来扩展 asm 语句:
asm("mov %0, #4" : "=l" (n3));
现在C编译器知道了,n3被修改了,并将生成预期结果:
foo:
push {r7, lr}
mov r7, #4
mov r0, r7
pop {r7, pc}
在Thumb状态下执行
请注意,根据给定的编译选项,编译器可能会切换到 Thumb 状态。 在 Thumb 状态下使用含有无效指令的内联汇编,将导致神秘的编译错误。
汇编代码尺寸
在大多数情况下,编译器将正确地确定汇编程序指令的大小,但汇编程序宏可能会使编译器感到困惑。 最好避免它们。
如果您感到困惑: 这是关于汇编语言宏,而不是C预处理宏。使用后者是可以的。
标签(Labels)
在汇编指令中,你可以使用标签作为跳转目的地。但是,你不能从一个汇编语句跳转到另一个(asm(...)为一个语句)。优化器对这些分支一无所知,可能会生成糟糕的代码。
预处理宏
内联汇编指令不能包含预处理宏,因为对于预处理器来说,这些指令只是字符串常量。
如果你的汇编代码必须要用被定义为宏的数值,参考上面“使用常量”这一章。
外部链接
有关内联汇编用法的更详细讨论,请参阅gcc用户手册。 gcc手册的最新版本总是在这里:
GCC online documentation- GNU Project
Copyright
Copyright (C) 2007-2013 by Harald Kipp. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation.
如果你认为有些事情没有解释清楚,甚至是错误的,请告诉我。
文档历史
Date (YMD) | Change | Thanks to |
2014/02/11 | Fixed the first constant example, where the constant must be an input operand. | spider391Tang |
2013/08/16 | Corrected the example code of specific register usage and added a new pitfall section about the same topic. | Sven Köhler |
2012/03/28 | Corrected the pitfall section about constant parameters and moved to the usage section. Added a preprocessor macros pitfall. Added this history. | enh |