【问题标题】:Is GCC broken when taking the address of an argument on ARM7TDMI?获取 ARM7TDMI 上的参数地址时 GCC 是否损坏?
【发布时间】:2010-09-06 21:52:52
【问题描述】:

我的 C 代码 sn-p 获取参数的地址并将其存储在易失性内存位置(预处理代码):

void foo(unsigned int x) {
    *(volatile unsigned int*)(0x4000000 + 0xd4) = (unsigned int)(&x);
}

int main() {
    foo(1);
    while(1);
}

我使用 SVN 版本的 GCC 来编译这段代码。在函数foo 的末尾,我希望将值1 存储在堆栈中,并且在0x40000d4 处有一个指向该值的地址。当我使用标志 -O0 编译而不进行优化时,我得到了预期的 ARM7TMDI 程序集输出(为方便起见而注释):

        .align  2
        .global foo
        .type   foo, %function
foo:
        @ Function supports interworking.
        @ args = 0, pretend = 0, frame = 8
        @ frame_needed = 0, uses_anonymous_args = 0
        @ link register save eliminated.
        sub     sp, sp, #8
        str     r0, [sp, #4]     @ 3. Store the argument on the stack
        mov     r3, #67108864
        add     r3, r3, #212
        add     r2, sp, #4       @ 4. Address of the stack variable
        str     r2, [r3, #0]     @ 5. Store the address at 0x40000d4
        add     sp, sp, #8
        bx      lr
        .size   foo, .-foo
        .align  2
        .global main
        .type   main, %function
main:
        @ Function supports interworking.
        @ args = 0, pretend = 0, frame = 0
        @ frame_needed = 0, uses_anonymous_args = 0
        stmfd   sp!, {r4, lr}
        mov     r0, #1           @ 1. Pass the argument in register 0
        bl      foo              @ 2. Call function foo
.L4:
        b       .L4
        .size   main, .-main
        .ident  "GCC: (GNU) 4.4.0 20080820 (experimental)"

它清楚地将参数首先存储在堆栈中,然后将其存储在0x40000d4。当我使用-O1 进行优化编译时,我得到了一些意想不到的结果:

        .align  2
        .global foo
        .type   foo, %function
foo:
        @ Function supports interworking.
        @ args = 0, pretend = 0, frame = 8
        @ frame_needed = 0, uses_anonymous_args = 0
        @ link register save eliminated.
        sub     sp, sp, #8
        mov     r2, #67108864
        add     r3, sp, #4        @ 3. Address of *something* on the stack
        str     r3, [r2, #212]    @ 4. Store the address at 0x40000d4
        add     sp, sp, #8
        bx      lr
        .size   foo, .-foo
        .align  2
        .global main
        .type   main, %function
main:
        @ Function supports interworking.
        @ args = 0, pretend = 0, frame = 0
        @ frame_needed = 0, uses_anonymous_args = 0
        stmfd   sp!, {r4, lr}
        mov     r0, #1           @ 1. Pass the argument in register 0
        bl      foo              @ 2. Call function foo
.L4:
        b       .L4
        .size   main, .-main
        .ident  "GCC: (GNU) 4.4.0 20080820 (experimental)"

这一次参数永远不会存储在堆栈中,即使堆栈中的 something 仍存储在 0x40000d4

这只是预期/未定义的行为吗?我是不是做错了什么,或者我实际上发现了 Compiler Bug™?

【问题讨论】:

    标签: c gcc arm assembly


    【解决方案1】:

    一旦你从foo()返回,x就消失了,任何指向它的指针都是无效的。随后使用这样的指针会导致 C 标准喜欢称之为“未定义的行为”,这意味着绝对允许编译器假设您不会取消引用它,或者(如果您坚持这样做)不需要生成代码远程做任何你可能期望的事情。如果您希望在foo() 返回后指向x 的指针仍然有效,则不能在foo 的堆栈上分配x,句号——即使您知道原则上没有任何东西破坏它的理由——因为这在 C 中是不允许的,无论它多久发生一次你所期望的。

    最简单的解决方案可能是使x 成为main() 中的局部变量(或任何其他具有足够长寿命范围的函数)并将地址传递给foo。您还可以将x 设为全局变量,或者使用malloc() 在堆上分配它,或者以更奇特的方式为其留出内存。您甚至可以尝试以某种(希望)更可移植的方式找出堆栈顶部的位置,并将您的数据显式存储在堆栈的某个部分,如果您确定不需要其他任何东西并且您'确信这是你真正需要做的。但正如您所发现的,您一直使用的方法不够可靠。

    【讨论】:

      【解决方案2】:

      我实际上并不认为编译器有错,尽管这是一个奇怪的情况。

      从代码分析的角度来看,它认为您存储了变量的地址,但该地址永远不会被取消引用,并且您不会跳出函数到可以使用您存储的地址的外部代码。当你退出函数时,堆栈的地址现在被认为是虚假的,因为它是一个不再存在的变量的地址。

      “volatile”关键字在 C 语言中的作用并不大,尤其是在多线程或硬件方面。它只是告诉编译器它必须进行访问。但是,根据数据流,由于 x 的值没有用户,因此没有理由将“1”存储在堆栈中。

      如果你写它可能会起作用

      void foo(unsigned int x) {
          volatile int y = x;
          *(volatile unsigned int*)(0x4000000 + 0xd4) = (unsigned int)(&y);
      }
      

      虽然它仍然可能是非法代码,因为一旦 foo 返回,y 的地址就被认为是无效的,但 DMA 系统的本质是独立于程序流引用该位置。

      【讨论】:

        【解决方案3】:

        所以你将本地堆栈变量的地址放入要使用的 DMA 控制器,然后从堆栈变量可用的函数返回?

        虽然这个 可能 适用于您的 main() 示例(因为您不再在堆栈上写入),但它稍后将无法在“真实”程序中工作 - 该值将被覆盖在调用另一个函数并再次使用堆栈时,在 DMA 访问它之前或期间。

        你需要有一个结构,或者一个全局变量,你可以在 DMA 访问它时使用它来存储这个值 - 否则它只会被破坏!

        -亚当

        【讨论】:

          【解决方案4】:

          需要注意的一点是,根据标准,强制转换是 r 值。 GCC 曾经允许它,但在最近的版本中,它已成为标准的坚持者。

          我不知道它是否会有所作为,但你应该试试这个:

          void foo(unsigned int x) {
              volatile unsigned int* ptr = (unsigned int*)(0x4000000 + 0xd4);
              *ptr = (unsigned int)(&x);
          }
          
          int main() {
              foo(1);
              while(1);
          }
          

          另外,我怀疑你是有意的,但你正在存储函数 local x 的地址(这是你传递的 int 的副本)。您可能希望 foo 采用“unsigned int *”并传递您真正想要存储的地址。

          所以我觉得更合适的解决方案是:

          void foo(unsigned int *x) {
              volatile unsigned int* ptr = (unsigned int*)(0x4000000 + 0xd4);
              *ptr = (unsigned int)(x);
          }
          
          int main() {
              int x = 1;
              foo(&x);
              while(1);
          }
          

          编辑:最后,如果您的代码因优化而中断,通常表明您的代码做错了。

          【讨论】:

            【解决方案5】:

            如果我现在能找到参考资料,我真该死,但我 99% 确定你 总是应该能够获取参数的地址,而且它已经完成了交给编译器来处理调用约定、寄存器使用等细节。

            确实,我会认为这是一个如此普遍的要求,以至于很难看出这可能存在普遍问题 - 我想知道是否是不稳定的指针破坏了优化。

            就个人而言,我可能会尝试这样做,看看它是否编译得更好:

            void foo(unsigned int x) 
            {
                volatile unsigned int* pArg = &x;
                *(volatile unsigned int*)(0x4000000 + 0xd4) = (unsigned int)pArg;
            }
            

            【讨论】:

            • 确保您可以获取它的地址(只有当您使用了 register 关键字时才能获取)。但是这是写一个本地的地址,它消失了,所以编译器优化它是完全合法的。
            【解决方案6】:

            Tomi Kyöstilä wrote

            Game Boy Advance 的开发。 我正在阅读它的 DMA 系统和 我通过创建来试验它 单色平铺位图。这个想法 是让索引颜色是 作为参数传递给函数 这将使用 DMA 填充瓷砖 用那种颜色。源地址 对于 DMA 传输存储在 0x40000d4.

            这对你来说是一件非常有效的事情,我可以看到你使用 -O1 优化得到的(意外的)代码是如何不起作用的。

            我看到您通过 -O0 优化获得的(预期的)代码符合您的预期 - 它将您想要的颜色值放在堆栈中,并将指向该颜色的指针放在 DMA 传输寄存器中。

            但是,即使您使用 -O0 优化获得的(预期的)代码也不起作用。 当 DMA 硬件开始使用该指针并使用它来读取所需的颜色时,堆栈上的该值(可能)早已被其他子例程或中断处理程序或两者覆盖。 因此,预期代码和意外代码都会导致相同的结果——DMA(可能)会获取错误的颜色。

            我认为您确实打算将颜色值存储在某个位置,以便在 DMA 完成读取之前保持安全。 所以一个全局变量,或者一个函数局部静态变量,比如

            // 警告:Three Star Programmer 在工作中

            // Warning: untested code.
            void foo(unsigned int x) {
                static volatile unsigned int color = x; // "static" so it's not on the stack
                volatile unsigned int** dma_register =
                    (volatile unsigned int**)(0x4000000 + 0xd4);
                *dma_register = &color;
            }
            
            int main() {
                foo(1);
                while(1);
            }
            

            这对你有用吗?

            你看我两次使用“volatile”,因为我想强制两个值以特定顺序写入。

            【讨论】:

              【解决方案7】:

              火花写道

              如果你认为你发现了一个错误 GCC 邮件列表会很高兴你 路过,但通常他们会发现 你知识中的一些漏洞是 责备和无情地嘲笑:(

              我想我先在这里试试运气,然后再去 GCC 邮件列表显示我的无能 :)


              亚当·戴维斯写道

              出于好奇,你在尝试什么 完成?

              我正在尝试开发 Game Boy Advance。我正在阅读它的 DMA 系统,并通过创建单色平铺位图进行了实验。想法是将索引颜色作为参数传递给函数,该函数将使用 DMA 用该颜色填充图块。 DMA 传输的源地址存储在0x40000d4


              迪恩会写

              就个人而言,我可能会试试这个看看 如果编译得更好:

              void foo(unsigned int x) 
              {
                  volatile unsigned int* pArg = &x;
                  *(volatile unsigned int*)(0x4000000 + 0xd4) = (unsigned int)pArg;
              }
              

              -O0 也可以正常工作,-O1 优化为与我在问题中发布的完全相同的 -O1 程序集。

              【讨论】:

                【解决方案8】:

                不是答案,只是为您提供更多信息。 我们在日常工作中运行 3.4.5 20051201 (Red Hat 3.4.5-2)。

                我们还注意到我们的一些代码(我无法在此处发布)在以下情况下停止工作 我们添加 -O1 标志。我们的解决方案是暂时删除该标志:(

                【讨论】:

                  【解决方案9】:

                  总的来说,我会说,这是一个有效的优化。 如果你想更深入地研究它,你可以用 -da 编译 这会生成一个 .c.Number.Passname,您可以在其中查看 rtl(gcc 中的中间表示)。在那里你可以看到哪个通道进行了哪个优化(可能只禁用一个,你不想拥有)

                  【讨论】:

                    【解决方案10】:

                    我认为 Even T. 有答案。您传入了一个变量,您不能在函数内获取该变量的地址,但是您可以获取该变量的副本的地址,顺便说一句,该变量通常是一个寄存器,因此它没有地址。一旦你离开那个函数,它就消失了,调用函数就会丢失它。如果您需要函数中的地址,则必须通过引用而不是按值传递,请发送地址。在我看来,错误在您的代码中,而不是 gcc。

                    顺便说一句,使用 *(volatile blah *)0xabcd 或任何其他方法尝试对寄存器进行编程最终会咬到你。 gcc 和大多数其他编译器都有这种不可思议的方式来准确地知道最坏的攻击时间。

                    说出你改变的那一天

                    *(volatile unsigned int *)0x12345 = someuintvariable;
                    

                    *(volatile unsigned int *)0x12345 = 0x12;
                    

                    一个好的编译器会意识到您只存储 8 位,没有理由为此浪费 32 位存储,具体取决于您指定的架构或当天该编译器的默认架构,所以它在它有权将其优化为 strb 而不是 str。

                    在被 gcc 和其他人烧了几十次之后,我不得不求助于这个问题:

                    .globl PUT32
                    PUT32:
                       str r1,[r0]
                       bx lr
                    
                    
                    
                    
                    
                       PUT32(0x12345,0x12);
                    

                    花费了几个额外的时钟周期,但我的代码在昨天、今天继续工作,并且明天将在任何优化标志下工作。不必重新访问旧代码并整夜安然入睡,值得在这里和那里多花几个时钟周期。

                    此外,如果您的代码在您为发布而不是为调试而编译时出现问题,这也意味着它很可能是您的代码中的错误。

                    【讨论】:

                      【解决方案11】:

                      这只是预期/未定义 行为?我做错什么了吗 还是我实际上找到了一个编译器 错误™?

                      没有错误,只是优化选项可以产生可能不起作用的奇怪代码的定义行为:)

                      编辑:

                      如果你认为你在 GCC 中发现了一个错误,邮件列表会很高兴你顺便过来,但通常他们会发现你的知识中有一些漏洞是责备和无情地嘲笑:(

                      在这种情况下,我认为可能是 -O 选项尝试使用快捷方式破坏了需要解决的代码。

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 2014-09-15
                        • 1970-01-01
                        • 1970-01-01
                        • 2020-03-25
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2013-03-09
                        相关资源
                        最近更新 更多