如果没有在 ASM 模板中指定,那么提供输入和输出操作数有什么意义呢?
What's the point of providing input and output operands if they are not specified in ASM template?
我在 u-boot/arch/arm/lib/semihosting.c 中找到了以下代码片段,它使用 bkpt
和其他指令并提供输入和输出操作数,即使它们未在 ASM 模板中指定:
static noinline long smh_trap(unsigned int sysnum, void *addr)
{
register long result asm("r0");
#if defined(CONFIG_ARM64)
asm volatile ("hlt #0xf000" : "=r" (result) : "0"(sysnum), "r"(addr));
#elif defined(CONFIG_CPU_V7M)
asm volatile ("bkpt #0xAB" : "=r" (result) : "0"(sysnum), "r"(addr));
#else
/* Note - untested placeholder */
asm volatile ("svc #0x123456" : "=r" (result) : "0"(sysnum), "r"(addr));
#endif
return result;
}
最小的、可验证的例子:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
register long result asm("r0");
void *addr = 0;
unsigned int sysnum = 0;
__asm__ volatile ("bkpt #0xAB" : "=r" (result) : "0"(sysnum), "r"(addr));
return EXIT_SUCCESS;
}
根据ARM架构参考手册bkpt
说明
根据我对 GCC 手册的阅读,采用单个 imm 参数
关于内联汇编的部分 GCC 不允许提供操作数,如果它们
模板中未指定。使用 -S
:
生成的输出程序集
.arch armv6
.eabi_attribute 28, 1
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 2
.eabi_attribute 30, 6
.eabi_attribute 34, 1
.eabi_attribute 18, 4
.file "bkpt-so.c"
.text
.align 2
.global main
.arch armv6
.syntax unified
.arm
.fpu vfp
.type main, %function
main:
@ args = 0, pretend = 0, frame = 8
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
str fp, [sp, #-4]!
add fp, sp, #0
sub sp, sp, #12
mov r3, #0
str r3, [fp, #-8]
mov r3, #0
str r3, [fp, #-12]
ldr r2, [fp, #-12]
ldr r3, [fp, #-8]
mov r0, r2
.syntax divided
@ 10 "bkpt-so.c" 1
bkpt #0xAB
@ 0 "" 2
.arm
.syntax unified
mov r3, #0
mov r0, r3
add sp, fp, #0
@ sp needed
ldr fp, [sp], #4
bx lr
.size main, .-main
.ident "GCC: (Raspbian 8.3.0-6+rpi1) 8.3.0"
.section .note.GNU-stack,"",%progbits
那么这一行中 "=r" (result) : "0"(sysnum), "r"(addr)
的意义何在:
__asm__ volatile ("bkpt #0xAB" : "=r" (result) : "0"(sysnum), "r"(addr));
?
这些指令用于从用户空间代码调用内核(或管理程序)(即执行 syscall)。它们导致 CPU 发出一个陷阱,该陷阱被内核拦截和处理。在预定义寄存器中传递的附加数据(在本例中为 r0
和 r1
)为内核的陷阱处理程序保留 "parameters"。
尽管此代码存在于像 U-BOOT 这样的知名项目中,但这并不能让人放心。该代码依赖于这样一个事实,即在 ARM 体系结构中,ABI (call standard) 在 r0
(参数 1)、r1
(参数 2)、[=13= 中传递前 4 个标量参数](参数 3)和 r3
(参数 4)。
Table 6.1 总结ABI:
U-BOOT 代码所做的假设是,当生成内联汇编时,传递给 r1
中的函数的 addr
仍然是相同的值。我认为这很危险,因为即使使用简单的非内联函数,GCC 也不能保证这种行为。我的观点是这段代码很脆弱,虽然它可能从未出现过问题,但理论上它可以。依赖底层编译器代码生成行为不是一个好主意。
我相信这样写会更好:
static noinline long smh_trap(unsigned int sysnum, void *addr)
{
register long result asm("r0");
register void *reg_r1 asm("r1") = addr;
#if defined(CONFIG_ARM64)
asm volatile ("hlt #0xf000" : "=r" (result) : "0"(sysnum), "r"(reg_r1) : "memory");
#elif defined(CONFIG_CPU_V7M)
asm volatile ("bkpt #0xAB" : "=r" (result) : "0"(sysnum), "r"(reg_r1) : "memory");
#else
/* Note - untested placeholder */
asm volatile ("svc #0x123456" : "=r" (result) : "0"(sysnum), "r"(reg_r1) : "memory");
#endif
return result;
}
此代码通过变量 (reg_r1
) 传递 addr
,为了内联汇编约束,该变量将被放入寄存器 r1
。在更高的优化级别上,编译器不会使用额外变量生成任何额外代码。我还放置了一个 memory
破坏器,因为在没有寄存器的情况下以这种方式通过寄存器传递内存地址不是一个好主意。如果有人要制作此功能的内联版本,这会带来问题。内存破坏器将确保在内联汇编 运行 之前将任何数据实现到内存中,并在之后必要时重新加载。
至于"=r" (result) : "0"(sysnum), "r"(addr)
做什么的问题是:
"=r"(result)
是一个输出约束,它告诉编译器内联汇编完成后寄存器 r0
中的值将被放入变量 addr
"0"(sysnum)
是一个输入约束,它告诉编译器 sysnum
将通过与约束 0 相同的寄存器传递到内联汇编代码中(约束 0 使用寄存器 r0
) .
"r"(addr)
通过寄存器传递 addr
并且假设它将与 U-BOOT 代码一起位于 r1
中。在我的版本中,它是这样明确定义的。
有关扩展内联汇编的操作数和约束的信息可以在 GCC documentation. You can find additional machine specific constraints here.
中找到
hlt
、bkpt
和 svc
都被用作系统调用以通过调试器执行系统服务(semihosting). You can find more documentation on semihosting here。不同的 ARM 体系结构使用稍微不同的机制。半主机系统调用的约定是 r0
包含系统调用号;r1
包含系统调用的第一个参数;系统调用放置一个 return在 return 到用户代码之前 r0
中的值。
我在 u-boot/arch/arm/lib/semihosting.c 中找到了以下代码片段,它使用 bkpt
和其他指令并提供输入和输出操作数,即使它们未在 ASM 模板中指定:
static noinline long smh_trap(unsigned int sysnum, void *addr)
{
register long result asm("r0");
#if defined(CONFIG_ARM64)
asm volatile ("hlt #0xf000" : "=r" (result) : "0"(sysnum), "r"(addr));
#elif defined(CONFIG_CPU_V7M)
asm volatile ("bkpt #0xAB" : "=r" (result) : "0"(sysnum), "r"(addr));
#else
/* Note - untested placeholder */
asm volatile ("svc #0x123456" : "=r" (result) : "0"(sysnum), "r"(addr));
#endif
return result;
}
最小的、可验证的例子:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
register long result asm("r0");
void *addr = 0;
unsigned int sysnum = 0;
__asm__ volatile ("bkpt #0xAB" : "=r" (result) : "0"(sysnum), "r"(addr));
return EXIT_SUCCESS;
}
根据ARM架构参考手册bkpt
说明
根据我对 GCC 手册的阅读,采用单个 imm 参数
关于内联汇编的部分 GCC 不允许提供操作数,如果它们
模板中未指定。使用 -S
:
.arch armv6
.eabi_attribute 28, 1
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 2
.eabi_attribute 30, 6
.eabi_attribute 34, 1
.eabi_attribute 18, 4
.file "bkpt-so.c"
.text
.align 2
.global main
.arch armv6
.syntax unified
.arm
.fpu vfp
.type main, %function
main:
@ args = 0, pretend = 0, frame = 8
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
str fp, [sp, #-4]!
add fp, sp, #0
sub sp, sp, #12
mov r3, #0
str r3, [fp, #-8]
mov r3, #0
str r3, [fp, #-12]
ldr r2, [fp, #-12]
ldr r3, [fp, #-8]
mov r0, r2
.syntax divided
@ 10 "bkpt-so.c" 1
bkpt #0xAB
@ 0 "" 2
.arm
.syntax unified
mov r3, #0
mov r0, r3
add sp, fp, #0
@ sp needed
ldr fp, [sp], #4
bx lr
.size main, .-main
.ident "GCC: (Raspbian 8.3.0-6+rpi1) 8.3.0"
.section .note.GNU-stack,"",%progbits
那么这一行中 "=r" (result) : "0"(sysnum), "r"(addr)
的意义何在:
__asm__ volatile ("bkpt #0xAB" : "=r" (result) : "0"(sysnum), "r"(addr));
?
这些指令用于从用户空间代码调用内核(或管理程序)(即执行 syscall)。它们导致 CPU 发出一个陷阱,该陷阱被内核拦截和处理。在预定义寄存器中传递的附加数据(在本例中为 r0
和 r1
)为内核的陷阱处理程序保留 "parameters"。
尽管此代码存在于像 U-BOOT 这样的知名项目中,但这并不能让人放心。该代码依赖于这样一个事实,即在 ARM 体系结构中,ABI (call standard) 在 r0
(参数 1)、r1
(参数 2)、[=13= 中传递前 4 个标量参数](参数 3)和 r3
(参数 4)。
Table 6.1 总结ABI:
U-BOOT 代码所做的假设是,当生成内联汇编时,传递给 r1
中的函数的 addr
仍然是相同的值。我认为这很危险,因为即使使用简单的非内联函数,GCC 也不能保证这种行为。我的观点是这段代码很脆弱,虽然它可能从未出现过问题,但理论上它可以。依赖底层编译器代码生成行为不是一个好主意。
我相信这样写会更好:
static noinline long smh_trap(unsigned int sysnum, void *addr)
{
register long result asm("r0");
register void *reg_r1 asm("r1") = addr;
#if defined(CONFIG_ARM64)
asm volatile ("hlt #0xf000" : "=r" (result) : "0"(sysnum), "r"(reg_r1) : "memory");
#elif defined(CONFIG_CPU_V7M)
asm volatile ("bkpt #0xAB" : "=r" (result) : "0"(sysnum), "r"(reg_r1) : "memory");
#else
/* Note - untested placeholder */
asm volatile ("svc #0x123456" : "=r" (result) : "0"(sysnum), "r"(reg_r1) : "memory");
#endif
return result;
}
此代码通过变量 (reg_r1
) 传递 addr
,为了内联汇编约束,该变量将被放入寄存器 r1
。在更高的优化级别上,编译器不会使用额外变量生成任何额外代码。我还放置了一个 memory
破坏器,因为在没有寄存器的情况下以这种方式通过寄存器传递内存地址不是一个好主意。如果有人要制作此功能的内联版本,这会带来问题。内存破坏器将确保在内联汇编 运行 之前将任何数据实现到内存中,并在之后必要时重新加载。
至于"=r" (result) : "0"(sysnum), "r"(addr)
做什么的问题是:
"=r"(result)
是一个输出约束,它告诉编译器内联汇编完成后寄存器r0
中的值将被放入变量addr
"0"(sysnum)
是一个输入约束,它告诉编译器sysnum
将通过与约束 0 相同的寄存器传递到内联汇编代码中(约束 0 使用寄存器r0
) ."r"(addr)
通过寄存器传递addr
并且假设它将与 U-BOOT 代码一起位于r1
中。在我的版本中,它是这样明确定义的。
有关扩展内联汇编的操作数和约束的信息可以在 GCC documentation. You can find additional machine specific constraints here.
中找到hlt
、bkpt
和 svc
都被用作系统调用以通过调试器执行系统服务(semihosting). You can find more documentation on semihosting here。不同的 ARM 体系结构使用稍微不同的机制。半主机系统调用的约定是 r0
包含系统调用号;r1
包含系统调用的第一个参数;系统调用放置一个 return在 return 到用户代码之前 r0
中的值。