ARM Thumb GCC已拆卸C.调用程序保存的寄存器未保存,并且立即加载和存储相同的寄存器

ktecyv1j  于 2022-11-13  发布在  其他
关注(0)|答案(2)|浏览(145)

环境:STM32 F469 Cortex-M4(ARMv7-M拇指-2),Win 10,GCC,STM32 CubeIDE;学习/尝试内联汇编和阅读反汇编、堆栈管理等,写入核心寄存器,观察寄存器的内容,检查堆栈指针周围的RAM,以了解事情是如何工作的。
我注意到,在某个时候,当我调用一个函数时,在一个被调用函数的开头,它接收到一个参数,为C函数生成的指令是“将R3存储在RAM地址X”,紧接着是“读取RAM地址X并存储在RAM中”。因此,它写入和阅读的是相同的值,R3没有改变。如果它只想将R3的值保存到堆栈上,那为什么要装回去呢
C代码,调用函数(main),我的代码:

asm volatile("      LDR R0,=#0x00000000\n"
            "       LDR R1,=#0x11111111\n"
            "       LDR R2,=#0x22222222\n"
            "       LDR R3,=#0x33333333\n"
            "       LDR R4,=#0x44444444\n"
            "       LDR R5,=#0x55555555\n"
            "       LDR R6,=#0x66666666\n"
            "       MOV R7,R7\n" //Stack pointer value is here, used for stack data access
            "       LDR R8,=#0x88888888\n"
            "       LDR R9,=#0x99999999\n"
            "       LDR R10,=#0xAAAAAAAA\n"
            "       LDR R11,=#0xBBBBBBBB\n"
            "       LDR R12,=#0xCCCCCCCC\n"
    );
testInt = addFifteen(testInt); //testInt=0x03; returns uint8_t, argument uint8_t

函数调用生成指令,将函数参数加载到R3,然后将其移动到R 0,然后通过链接分支到addFifteen。因此,当我输入addFifteen时,R 0和R3的值为0x 03(testInt)。到目前为止一切顺利。以下是函数调用的外观:

testInt = addFifteen(testInt);
08000272:   ldrb    r3, [r7, #11]
08000274:   mov     r0, r3
08000276:   bl      0x80001f0 <addFifteen>

所以我进入addFifteen,我的addFifteen的C代码:

uint8_t addFifteen(uint8_t input){
    return (input + 15U);
}

其拆卸:

addFifteen:
080001f0:   push    {r7}
080001f2:   sub     sp, #12
080001f4:   add     r7, sp, #0
080001f6:   mov     r3, r0
080001f8:   strb    r3, [r7, #7]
080001fa:   ldrb    r3, [r7, #7]
080001fc:   adds    r3, #15
080001fe:   uxtb    r3, r3
08000200:   mov     r0, r3
08000202:   adds    r7, #12
08000204:   mov     sp, r7
08000206:   ldr.w   r7, [sp], #4
0800020a:   bx      lr

我主要感兴趣的是1f 8和1fa行。它将R3存储在堆栈上,然后将新写入的值加载回仍然保存该值的寄存器。
问题是:
1.“将寄存器A存入RAM X,然后从RAM X读取值存入寄存器A”的目的是什么?读指令似乎没有任何作用。请确保RAM写操作已完成?

  1. Push{r7}指令使堆栈4字节对齐,而不是8字节对齐。但在该指令之后,我们立即将SP递减12(字节),因此它再次变为8字节对齐。因此,此行为是可以的。此语句是否正确?如果在这两条指令之间发生中断,该怎么办?在ISR堆栈期间,对齐是否会在ISR期间得到修复?
    1.从我读到的关于调用方/被调用方保存的寄存器(很难找到任何组织良好的信息,如果你有好的材料,请分享一个链接),至少R 0-R3必须放置在堆栈上,当我调用一个函数。然而,很容易注意到,在这种情况下,没有寄存器被推到堆栈上,我通过检查堆栈指针周围的内存来验证它,很容易注意到0x 11111111和0x 22222222,但它们并不存在,没有任何东西把它们推到那里,在调用函数之前,R 0和R3中的值已经永远消失了,为什么在调用函数之前,没有任何寄存器被推到堆栈上呢?当addFifteen返回时,我希望R3是0x 33333333,因为这是函数调用之前的情况,但是即使在分支到addFifteen之前,这个值也会被随意覆盖。为什么GCC没有生成指令将R 0-R3压入堆栈,并且只在分支到addFifteen之后才生成?
    如果您需要一些编译器设置,请告诉我在Eclipse(STM32 CubeIDE)中的何处可以找到它们,以及您在那里到底需要什么,我将很乐意提供它们并将它们添加到此处的问题中。
h7appiyu

h7appiyu1#

uint8_t addFifteen(uint8_t input){
    return (input + 15U);
}

你在这里看到的是未优化的,至少在gnu中,输入和局部变量在堆栈上获得了一个内存位置。

00000000 <addFifteen>:
   0:   b480        push    {r7}
   2:   b083        sub sp, #12
   4:   af00        add r7, sp, #0
   6:   4603        mov r3, r0
   8:   71fb        strb    r3, [r7, #7]
   a:   79fb        ldrb    r3, [r7, #7]
   c:   330f        adds    r3, #15
   e:   b2db        uxtb    r3, r3
  10:   4618        mov r0, r3
  12:   370c        adds    r7, #12
  14:   46bd        mov sp, r7
  16:   bc80        pop {r7}
  18:   4770        bx  lr

你在r3中看到的是,输入变量input进入r 0,由于某种原因,代码没有被优化,它进入r3,然后保存在堆栈上的内存位置。
设置堆栈

00000000 <addFifteen>:
   0:   b480        push    {r7}
   2:   b083        sub sp, #12
   4:   af00        add r7, sp, #0

将输入保存到堆栈

6:   4603        mov r3, r0
   8:   71fb        strb    r3, [r7, #7]

现在我们可以开始在函数中实现代码,该函数需要对输入函数进行数学运算,因此进行数学运算

a:   79fb        ldrb    r3, [r7, #7]
   c:   330f        adds    r3, #15

将结果转换为无符号字符。

e:   b2db        uxtb    r3, r3

现在准备返回值

10:   4618        mov r0, r3

并清理和返回

12:   370c        adds    r7, #12
  14:   46bd        mov sp, r7
  16:   bc80        pop {r7}
  18:   4770        bx  lr

现在,如果我告诉它不要使用帧指针(只是浪费寄存器)。

00000000 <addFifteen>:
   0:   b082        sub sp, #8
   2:   4603        mov r3, r0
   4:   f88d 3007   strb.w  r3, [sp, #7]
   8:   f89d 3007   ldrb.w  r3, [sp, #7]
   c:   330f        adds    r3, #15
   e:   b2db        uxtb    r3, r3
  10:   4618        mov r0, r3
  12:   b002        add sp, #8
  14:   4770        bx  lr

你仍然可以看到实现函数的每一个基本步骤。未优化。
现在如果你优化

00000000 <addFifteen>:
   0:   300f        adds    r0, #15
   2:   b2c0        uxtb    r0, r0
   4:   4770        bx  lr

它会去除所有多余的东西。
二号。
是的,我同意这看起来是错误的,但是gnu肯定不会一直保持堆栈对齐,所以这看起来是错误的。但是我还没有读过arm调用约定的细节。我也没有读过gcc的解释是什么。当然,他们可能会要求一个规范,但是在一天结束的时候,编译器的作者为他们的编译器选择调用约定。他们没有义务武装或情报或其他符合任何规范。他们的选择,就像C语言本身,有很多地方是实现定义的,gnu用一种方式实现C语言,而其他的用另一种方式。2也许这是一样的。同样的方法也适用于将传入的变量保存到堆栈中,我们将看到llvm/clang没有这样做。
三号。
r 0-r3和另外两个寄存器可以称为调用者保存的寄存器,但更好的理解方式是volatile。被调用者可以自由地修改它们,而不需要保存它们。这不是保存r 0寄存器的情况,而是r 0代表一个变量,您在函数实现高级代码时管理该变量。
比如说

unsigned int fun1 ( void );
unsigned int fun0 ( unsigned int x )
{
    return(fun1()+x);
}
00000000 <fun0>:
   0:   b510        push    {r4, lr}
   2:   4604        mov r4, r0
   4:   f7ff fffe   bl  0 <fun1>
   8:   4420        add r0, r4
   a:   bd10        pop {r4, pc}

x在r 0中出现,我们需要保存这个值,直到调用fun 1()。r 0可以被fun 1()销毁/修改。所以在这种情况下,它们保存r4,而不是r 0,并在r4中保留x。
clang也能做到这一点

00000000 <fun0>:
   0:   b5d0        push    {r4, r6, r7, lr}
   2:   af02        add r7, sp, #8
   4:   4604        mov r4, r0
   6:   f7ff fffe   bl  0 <fun1>
   a:   1900        adds    r0, r0, r4
   c:   bdd0        pop {r4, r6, r7, pc}

回到你的工作岗位。
clang,unoptimized也会将输入变量保存在内存(堆栈)中。

00000000 <addFifteen>:
   0:   b081        sub sp, #4
   2:   f88d 0003   strb.w  r0, [sp, #3]
   6:   f89d 0003   ldrb.w  r0, [sp, #3]
   a:   300f        adds    r0, #15
   c:   b2c0        uxtb    r0, r0
   e:   b001        add sp, #4
  10:   4770        bx  lr

你可以看到同样的步骤,准备堆栈,存储输入变量。2取输入变量做数学运算。3准备返回值。4清理,返回。
优化的Clang/llvm:

00000000 <addFifteen>:
   0:   300f        adds    r0, #15
   2:   b2c0        uxtb    r0, r0
   4:   4770        bx  lr

碰巧与gnu相同。不期望任何两个不同的编译器生成相同的代码,也不期望任何两个版本的相同编译器生成相同的代码。
1.未优化,输入和局部变量(在这个例子中没有)得到一个栈上的home.所以你看到的是输入变量作为函数设置的一部分被放到栈上的home.然后函数本身想对这个变量进行操作,所以,未经优化,它需要从内存中获取该值以创建中间变量(在这种情况下,它没有在堆栈上得到一个home)等等。你也可以在volatile变量中看到这一点。它们将被写入内存,然后读回,然后修改,然后写入内存,再读回,等等...
1.是,我同意,但是我没有看过规范,说到底,这是gcc的调用约定或者他们选择使用的一些规范的解释。他们一直在这样做(没有100%对齐),并且不会失败。对于所有被调用的函数,它们在函数被调用时都是对齐的。由gcc生成的arm代码中的中断并不总是对齐的。自从他们采用了那个规范就一直这样。
1.根据定义,r 0-r3等是易失的。被调用者可以随意修改它们。被调用者只需要在IT需要它们时保存/保留它们。在未优化和优化的情况下,只有r 0对你的函数很重要,它是输入变量,用于返回值。你在我创建的函数中看到,即使在优化时,输入变量也被保留以备后用。但是,根据定义,调用者假定这些寄存器被被调用函数破坏,并且被调用函数可以破坏这些寄存器的内容,并且不需要保存它们。
至于内联汇编,它是一种不同于“真实的的”汇编语言的汇编语言。我认为在准备好之前,您还有很长的路要走,但也许没有。经过几十年的不断裸机工作,我没有发现内联汇编的真正用例,我看到的例子是懒惰,避免允许真实的的汇编进入make系统,或者避免编写真正的汇编语言。我把它看作是一个酥油高手的功能,人们使用像工会和位域。

在gnu中,对于arm,至少有四种不兼容的汇编语言。不统一语法的真实的汇编,统一的语法真实的的汇编。当你用gcc而不是as来汇编,然后为gcc内联汇编时,你看到的汇编语言。尽管声明兼容clang arm汇编语言并不是100%兼容gnu汇编语言和llvm/clang没有一个单独的汇编器,你把它提供给编译器。arm多年来的各种工具链都有完全不兼容的汇编语言,gnu for arm。这都是预料之中的,也是正常的。汇编语言是特定于工具的,而不是目标。
在你学习内联汇编语言之前,先学习一些真实的的汇编语言。公平地说,也许你确实是这样做的,而且可能做得很好。这个问题是关于发现编译器如何生成代码,以及当你发现它不是一对一的时候看起来有多奇怪(所有的工具在所有的情况下都从相同的输入生成相同的输出)。
对于内联asm,虽然您可以指定寄存器,但根据您正在执行的操作,您通常希望让编译器选择寄存器,内联汇编的大部分工作不是汇编,而是特定编译器用来与之接口的语言......这是编译器特定的,转移到另一个编译器,期望是一个全新的语言学习。虽然在汇编程序之间移动也是一种全新的语言,但至少指令本身的语法往往是相同的,而语言的差异则体现在其他方面,标签和指令之类的。如果幸运的话,它是一个工具链而不仅仅是一个汇编程序,你可以看看编译器的输出来开始理解语言,并将其与你能找到的任何文档进行比较。Gnus文档在这种情况下相当糟糕,因此需要大量的逆向工程。同时,你更有可能成功地使用GNU工具,而不是因为它们更好,在许多情况下它们并不是,而是因为纯粹的用户基础和跨越目标和几十年历史的共同特性。
通过创建模拟C函数来查看使用了哪些寄存器等,我会非常擅长将asm与C进行接口。而且/或者更好的是,用C实现它,编译它,然后手动修改/改进/编译器的任何输出(你不需要成为一个大师来击败编译器,也许是一致的,但相当多的时候,你可以很容易地看到可以对gcc的输出进行的改进,gcc在过去的几个版本中变得越来越差,它并没有变得更好,就像你在这个网站上不时看到的那样)。在这个工具链和目标以及编译器如何工作的asm方面做得很好,然后也许可以学习gnu内联汇编语言。

vxf3dgd4

vxf3dgd42#

1.我不确定这样做有什么特定的目的,这只是编译器找到的一种解决方案。
例如代码:

unsigned int f(unsigned int a)
{ 
   return sqrt(a + 1);
}

使用ARM GCC 9 NONE编译,优化级别为-O 0,以便:

push    {r7, lr}
    sub     sp, sp, #8
    add     r7, sp, #0
    str     r0, [r7, #4]
    ldr     r3, [r7, #4]
    adds    r3, r3, #1
    mov     r0, r3
    bl      __aeabi_ui2d
    mov     r2, r0
    mov     r3, r1
    mov     r0, r2
    mov     r1, r3
    bl      sqrt
    ...

在-O 1层中:

push    {r3, lr}
    adds    r0, r0, #1
    bl      __aeabi_ui2d
    bl      sqrt
    ...

正如您所看到的,asm在-O 1中更容易理解:将参数存储在R 0中,添加1,调用函数。
1.硬件在异常期间支持非对齐堆栈。请参见here
1.“调用者保存”寄存器不一定需要存储在堆栈上,它取决于调用者是否需要存储它们。在这里,你混合了(如果我理解正确的话)C和汇编:因此在切换回C:要么你把值存储在被调用者保存的寄存器中(然后你通过约定知道编译器将在函数调用期间存储它们),要么你自己把它们存储在堆栈上。

相关问题