我发现下面这段代码是在 u-bootarcharmlibsemihosting. 使用 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
指令采用一个单一的Imm参数,根据我的阅读。GCC手册中关于在线装配的部分 如果操作数没有在模板中指定,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));
?
尽管这段代码存在于像U-BOOT这样的知名项目中,但并没有给人以信心。该代码是依靠的事实,与ARM架构,即 ABI(呼叫标准) 中传递前4个标量参数。r0
(论点1)。r1
(论点2)。r2
(论点3),以及 r3
(论点4)。
表6.1总结了ABI。
U-BOOT代码的假设是: addr
中传递给函数的 r1
在生成内联装配体时仍然是相同的值。我认为这很危险,因为即使是一个简单的非内联函数,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;
}
这段代码通过 addr
通过一个变量(reg_r1
),将被放入寄存器 r1
用于内联装配约束的目的。在更高的优化级别上,编译器不会用额外的变量生成任何额外的代码。我还将一个 memory
clobber,因为在没有内存地址的情况下通过寄存器传递内存地址不是一个好主意。如果有人要制作这个函数的内联版本,这就会带来一个问题。内存clobber会确保在内联汇编运行之前,任何数据都会被实现到内存中,如果有必要的话,会在必要的时候在之后重新加载。
至于关于什么的问题 "=r" (result) : "0"(sysnum), "r"(addr)
的作用是。
"=r"(result)
是一个输出约束,它告诉编译器,寄存器中的值 r0
在内联装配完成后,将被放置在可变的 addr
"0"(sysnum)
是一个输入约束,告诉编译器 sysnum
将通过与约束0相同的寄存器传递到内联装配代码中(约束0使用的是寄存器--------)。r0
). "r"(addr)
通行证 addr
通过一个寄存器,并假设它将会在 r1
与U-BOOT代码。在我的版本中,它是显式地这样定义的。关于扩展的内联装配的操作数和约束的信息可以在 GCC文件. 您可以找到更多的机器特定约束条件 此处.
hlt
, bkpt
和 svc
都被用作系统调用,通过调试器执行系统服务 (半托管). 你可以找到更多关于半托管的文档 此处. 不同的ARM架构使用的机制略有不同。半托管系统调用的惯例是 r0
包含系统调用号。r1
包含系统调用的第一个参数;系统调用将返回值置于 r0
然后再返回到用户代码。
这些指令用于从用户空间代码中调用内核(或管理程序)(即执行一个 系统调用). 它们导致CPU发出一个陷阱,被内核拦截并处理。在预定义寄存器中传递的附加数据(r0
和 r1
在本例中)保存了内核陷阱处理程序的 "参数"。