跳出内联汇编在 AVR32 上转到了错误的目标
Jump out of inline assembly goes to the wrong target on AVR32
我们正在使用 AtmelStudio 7.0.1645 为 Atmel AVR32 / UC3C0512C 开发应用程序。在做一些基本测试时,我发现了一些很奇怪的东西。
请考虑以下代码(我知道这是糟糕的风格并且不常见,但这不是这里的重点):
float GetAtan2f(float p_f_y,
float p_f_x)
{
unsigned int l_ui_x,
l_ui_y,
l_ui_Sign_x,
l_ui_Sign_y,
l_ui_Result;
float l_f_Add,
l_f_Result;
asm volatile(
"RJMP GETATAN2_EXIT \n"
:
: /* 0 */ "m" (p_f_y),
/* 1 */ "m" (p_f_x)
: "cc", "memory", "r0", "r1", "r2", "r3", "r5"
);
GETATAN2_EXIT:
return (l_f_Result);
}
在查看该代码的反汇编时(编译/链接后),我发现以下内容:
Disassembly of section .text.GetAtan2f:
00078696 <GetAtan2f>:
78696: eb cd 40 af pushm r0-r3,r5,r7,lr
7869a: 1a 97 mov r7,sp
7869c: 20 9d sub sp,36
7869e: ef 4c ff e0 st.w r7[-32],r12
786a2: ef 4b ff dc st.w r7[-36],r11
786a6: e0 8f 00 00 bral 786a6 <GetAtan2f+0x10>
786aa: ee f8 ff fc ld.w r8,r7[-4]
786ae: 10 9c mov r12,r8
786b0: 2f 7d sub sp,-36
786b2: e3 cd 80 af ldm sp++,r0-r3,r5,r7,pc
我们注意到 rjmp
已经变成 bral
- 完全可以接受,只是同一事物的另一个助记符。
但是当查看该行中的分支目标时,我们还注意到这将产生一个无限循环,这显然不应该。它应该分支到 786aa
(这是函数 return 的开始)而不是 786a6
.
如果我更改代码使其显示为
float GetAtan2f(float p_f_y,
float p_f_x)
{
unsigned int l_ui_x,
l_ui_y,
l_ui_Sign_x,
l_ui_Sign_y,
l_ui_Result;
float l_f_Add,
l_f_Result;
asm volatile(
"RJMP GETATAN2_EXIT \n"
:
: /* 0 */ "m" (p_f_y),
/* 1 */ "m" (p_f_x)
: "cc", "memory", "r0", "r1", "r2", "r3", "r5"
);
asm volatile(
"GETATAN2_EXIT: \n"
:
:
: "cc", "memory"
);
return (l_f_Result);
}
它按预期工作,即反汇编现在显示为
Disassembly of section .text.GetAtan2f:
00078696 <GETATAN2_EXIT-0x12>:
78696: eb cd 40 af pushm r0-r3,r5,r7,lr
7869a: 1a 97 mov r7,sp
7869c: 20 9d sub sp,36
7869e: ef 4c ff e0 st.w r7[-32],r12
786a2: ef 4b ff dc st.w r7[-36],r11
786a6: c0 18 rjmp 786a8 <GETATAN2_EXIT>
000786a8 <GETATAN2_EXIT>:
786a8: ee f8 ff fc ld.w r8,r7[-4]
786ac: 10 9c mov r12,r8
786ae: 2f 7d sub sp,-36
786b0: e3 cd 80 af ldm sp++,r0-r3,r5,r7,pc
我们注意到分支目标现在是正确的。
所以内联汇编器显然不知道 C 标签(即不在内联汇编中的标签),这本身就是 O.K。 - 吸取教训。
但除此之外它在遇到未知(未定义)标签时不会发出警告或抛出错误,而是通过在分支/跳转到此类标签时仅使用偏移量 0 来产生无限循环.
我认为后者是一个灾难性的错误。这可能意味着(在没有任何警告的情况下)每当我在内联汇编代码中使用未定义的标签(例如,由于拼写错误)时,我的软件就会陷入无限循环。
我能做些什么吗?
如果您不告诉编译器您的 asm
语句的另一边可能不会执行,编译器会假设是这种情况。
所以你的两个例子都是不安全的,幸运的是第二个没有破坏任何东西,因为函数太简单了。
我完全不确定你的代码是如何编译的; C 本地标签通常不会显示为具有相同名称的 asm 标签。如果 compiler-generated 代码完全使用它们,gcc 使用 .L1
之类的名称,这与它为 if()
和 for
发明的 b运行ch 目标相同/while
循环。 @Kampi 使用 AtmelStudios 7.0.1931 报告您的源链接器错误。
也许您实际上正在查看 non-linked .o
,其中 b运行ch 目标只是一个由链接器填充的占位符。 (并且对未定义符号的引用是等待发生的链接器错误)。 e0 8f 00 00
的编码当然符合:assembler 在编译器给它的 .s
中没有找到 b运行ch 目标标签,所以它把它当作一个外部符号,并使用了一个带有更多位移字节的 b运行ch。显然在 AVR32 上,相对 b运行ch 位移是相对于 b运行ch 指令的 start 的,不像许多 ISA,它是相对于指令结尾的b运行ch。 (即指令为 decoded/executed 时的 PC 已经递增。)
所以这可以解释你没有链接器错误(因为你从来没有 运行 链接器),并且看到了一个伪造的 b运行ch 目标。 更新:这个是被链接,但是链接到一个库中。所以库本身仍然有一个未解析的符号。
另一个内联 asm 语句 中定义的目标 出现在编译器的 asm 输出中,因此 assembler 找到它并可以使用短 rjmp
.
(一些 assemblers 通过要求 extern foo
声明来帮助你发现这样的错误。GAS 没有;它只是假设任何未定义的符号都是 extern
。GAS 语法来自传统的 Unix assemblers 被设计为 assemble 编译器输出,古代编译器一次只编译一个 C 函数(不是 whole-file 优化)不知道一个定义是否是函数将出现在这个 .c
文件或一个单独的 .c
文件中。所以这个语法允许 one-pass 在没有足够内存的机器上编译 C 返回并添加 extern
声明稍后在 asm 输出中未定义的符号。)
GNU C asm goto
使它安全
GNU C 内联汇编确实有跳出 inline-asm 语句(到 C 标签)的语法。 https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#GotoLabels. And see an example on SO: Labels in GCC inline assembly。 (使用 x86 指令,但 asm 模板的内容与您如何使用 asm goto
语法无关。)
在 condition-code / flag 输出没有 GCC6 语法的目标上,在内嵌 asm 中使用条件 b运行ch 跳转到 some_label: return true;
或落入 return false;
。 ()
但是根据 the commit message 说明 Linux 内核放弃 AVR32 支持的原因,AVR32 gcc 停滞在 gcc4.2。 asm goto
只出现在gcc4.5.
除非 AtmelStudio 编译器(基于?)更新的 gcc,否则您无法安全地执行此操作。
我们正在使用 AtmelStudio 7.0.1645 为 Atmel AVR32 / UC3C0512C 开发应用程序。在做一些基本测试时,我发现了一些很奇怪的东西。
请考虑以下代码(我知道这是糟糕的风格并且不常见,但这不是这里的重点):
float GetAtan2f(float p_f_y,
float p_f_x)
{
unsigned int l_ui_x,
l_ui_y,
l_ui_Sign_x,
l_ui_Sign_y,
l_ui_Result;
float l_f_Add,
l_f_Result;
asm volatile(
"RJMP GETATAN2_EXIT \n"
:
: /* 0 */ "m" (p_f_y),
/* 1 */ "m" (p_f_x)
: "cc", "memory", "r0", "r1", "r2", "r3", "r5"
);
GETATAN2_EXIT:
return (l_f_Result);
}
在查看该代码的反汇编时(编译/链接后),我发现以下内容:
Disassembly of section .text.GetAtan2f:
00078696 <GetAtan2f>:
78696: eb cd 40 af pushm r0-r3,r5,r7,lr
7869a: 1a 97 mov r7,sp
7869c: 20 9d sub sp,36
7869e: ef 4c ff e0 st.w r7[-32],r12
786a2: ef 4b ff dc st.w r7[-36],r11
786a6: e0 8f 00 00 bral 786a6 <GetAtan2f+0x10>
786aa: ee f8 ff fc ld.w r8,r7[-4]
786ae: 10 9c mov r12,r8
786b0: 2f 7d sub sp,-36
786b2: e3 cd 80 af ldm sp++,r0-r3,r5,r7,pc
我们注意到 rjmp
已经变成 bral
- 完全可以接受,只是同一事物的另一个助记符。
但是当查看该行中的分支目标时,我们还注意到这将产生一个无限循环,这显然不应该。它应该分支到 786aa
(这是函数 return 的开始)而不是 786a6
.
如果我更改代码使其显示为
float GetAtan2f(float p_f_y,
float p_f_x)
{
unsigned int l_ui_x,
l_ui_y,
l_ui_Sign_x,
l_ui_Sign_y,
l_ui_Result;
float l_f_Add,
l_f_Result;
asm volatile(
"RJMP GETATAN2_EXIT \n"
:
: /* 0 */ "m" (p_f_y),
/* 1 */ "m" (p_f_x)
: "cc", "memory", "r0", "r1", "r2", "r3", "r5"
);
asm volatile(
"GETATAN2_EXIT: \n"
:
:
: "cc", "memory"
);
return (l_f_Result);
}
它按预期工作,即反汇编现在显示为
Disassembly of section .text.GetAtan2f:
00078696 <GETATAN2_EXIT-0x12>:
78696: eb cd 40 af pushm r0-r3,r5,r7,lr
7869a: 1a 97 mov r7,sp
7869c: 20 9d sub sp,36
7869e: ef 4c ff e0 st.w r7[-32],r12
786a2: ef 4b ff dc st.w r7[-36],r11
786a6: c0 18 rjmp 786a8 <GETATAN2_EXIT>
000786a8 <GETATAN2_EXIT>:
786a8: ee f8 ff fc ld.w r8,r7[-4]
786ac: 10 9c mov r12,r8
786ae: 2f 7d sub sp,-36
786b0: e3 cd 80 af ldm sp++,r0-r3,r5,r7,pc
我们注意到分支目标现在是正确的。
所以内联汇编器显然不知道 C 标签(即不在内联汇编中的标签),这本身就是 O.K。 - 吸取教训。
但除此之外它在遇到未知(未定义)标签时不会发出警告或抛出错误,而是通过在分支/跳转到此类标签时仅使用偏移量 0 来产生无限循环.
我认为后者是一个灾难性的错误。这可能意味着(在没有任何警告的情况下)每当我在内联汇编代码中使用未定义的标签(例如,由于拼写错误)时,我的软件就会陷入无限循环。
我能做些什么吗?
如果您不告诉编译器您的 asm
语句的另一边可能不会执行,编译器会假设是这种情况。
所以你的两个例子都是不安全的,幸运的是第二个没有破坏任何东西,因为函数太简单了。
我完全不确定你的代码是如何编译的; C 本地标签通常不会显示为具有相同名称的 asm 标签。如果 compiler-generated 代码完全使用它们,gcc 使用 .L1
之类的名称,这与它为 if()
和 for
发明的 b运行ch 目标相同/while
循环。 @Kampi 使用 AtmelStudios 7.0.1931 报告您的源链接器错误。
也许您实际上正在查看 non-linked .o
,其中 b运行ch 目标只是一个由链接器填充的占位符。 (并且对未定义符号的引用是等待发生的链接器错误)。 e0 8f 00 00
的编码当然符合:assembler 在编译器给它的 .s
中没有找到 b运行ch 目标标签,所以它把它当作一个外部符号,并使用了一个带有更多位移字节的 b运行ch。显然在 AVR32 上,相对 b运行ch 位移是相对于 b运行ch 指令的 start 的,不像许多 ISA,它是相对于指令结尾的b运行ch。 (即指令为 decoded/executed 时的 PC 已经递增。)
所以这可以解释你没有链接器错误(因为你从来没有 运行 链接器),并且看到了一个伪造的 b运行ch 目标。 更新:这个是被链接,但是链接到一个库中。所以库本身仍然有一个未解析的符号。
另一个内联 asm 语句 中定义的目标 出现在编译器的 asm 输出中,因此 assembler 找到它并可以使用短 rjmp
.
(一些 assemblers 通过要求 extern foo
声明来帮助你发现这样的错误。GAS 没有;它只是假设任何未定义的符号都是 extern
。GAS 语法来自传统的 Unix assemblers 被设计为 assemble 编译器输出,古代编译器一次只编译一个 C 函数(不是 whole-file 优化)不知道一个定义是否是函数将出现在这个 .c
文件或一个单独的 .c
文件中。所以这个语法允许 one-pass 在没有足够内存的机器上编译 C 返回并添加 extern
声明稍后在 asm 输出中未定义的符号。)
GNU C asm goto
使它安全
GNU C 内联汇编确实有跳出 inline-asm 语句(到 C 标签)的语法。 https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#GotoLabels. And see an example on SO: Labels in GCC inline assembly。 (使用 x86 指令,但 asm 模板的内容与您如何使用 asm goto
语法无关。)
在 condition-code / flag 输出没有 GCC6 语法的目标上,在内嵌 asm 中使用条件 b运行ch 跳转到 some_label: return true;
或落入 return false;
。 (
但是根据 the commit message 说明 Linux 内核放弃 AVR32 支持的原因,AVR32 gcc 停滞在 gcc4.2。 asm goto
只出现在gcc4.5.
除非 AtmelStudio 编译器(基于?)更新的 gcc,否则您无法安全地执行此操作。