3

我在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指令采用单个 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));

?

4

2 回答 2

3

尽管该代码存在于像 U-BOOT 这样的知名项目中,但这并不能增强信心。该代码依赖于这样一个事实,即ABI(调用标准)r0在(参数 1)、r1(参数 2)、r2(参数 3)和r3(参数 4)中传递前 4 个标量参数的 ARM 体系结构。

表 6.1 总结了 ABI:

在此处输入图像描述

U-BOOT 代码所做的假设是,当生成内联汇编时addr,传递给函数 inr1的值仍然是相同的值。我认为这很危险,因为即使使用简单的非内联函数 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中,用于内联汇编约束。在更高的优化级别上,编译器不会使用额外的变量生成任何额外的代码。我还放置了一个memoryclobber,因为在没有寄存器的情况下以这种方式通过寄存器传递内存地址不是一个好主意。如果有人要制作此函数的内联版本,则会出现问题。内存破坏器将确保在运行内联汇编之前将任何数据实现到内存中,并在必要时在必要时重新加载。


至于做什么的问题"=r" (result) : "0"(sysnum), "r"(addr)是:

  • "=r"(result)是一个输出约束,它告诉编译器r0在内联汇编完成后寄存器中的值将被放置在变量中addr
  • "0"(sysnum)是一个输入约束,它告诉编译器sysnum将通过与约束 0 相同的寄存器传递到内联汇编代码中(约束 0 正在使用寄存器r0)。
  • "r"(addr)通过addr一个寄存器,假设它将r1与 U-BOOT 代码一起。在我的版本中,它是明确定义的。

有关扩展内联汇编的操作数和约束的信息可以在GCC 文档中找到。您可以在此处找到其他特定于机器的限制。

hltbkptsvc都被用作系统调用,以通过调试器(半主机)执行系统服务。您可以在此处找到有关半主机的更多文档。不同的 ARM 体系结构使用稍微不同的机制。半主机系统调用的约定是r0包含系统调用号;r1包含系统调用的第一个参数;r0系统调用在返回用户代码之前放入一个返回值。

于 2020-06-02T17:10:38.897 回答
2

这些指令用于从用户空间代码调用内核(或管理程序)(即执行系统调用)。它们导致 CPU 发出一个陷阱,该陷阱被内核拦截和处理。在预定义寄存器中传递的附加数据(r0r1这种情况下)包含内核陷阱处理程序的“参数”。

于 2020-06-02T15:55:52.530 回答