5

My C code snippet takes the address of an argument and stores it in a volatile memory location (preprocessed code):

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

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

I used an SVN version of GCC for compiling this code. At the end of function foo I would expect to have the value 1 stored in the stack and, at 0x40000d4, an address pointing to that value. When I compile without optimizations using the flag -O0, I get the expected ARM7TMDI assembly output (commented for your convenience):

        .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)"

It clearly stores the argument first on the stack and from there stores it at 0x40000d4. When I compile with optimizations using -O1, I get something unexpected:

        .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)"

This time the argument is never stored on the stack even though something from the stack is still stored at 0x40000d4.

Is this just expected/undefined behaviour? Have I done something wrong or have I in fact found a Compiler Bug™?

4

11 回答 11

7

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

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

于 2008-11-16T02:43:43.970 回答
4

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

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

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

-亚当

于 2008-09-10T13:34:59.160 回答
4

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

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

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

如果你写它可能会起作用

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

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

于 2008-09-16T04:48:09.143 回答
3

需要注意的一件事是,根据标准,演员表是 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);
}

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

于 2008-11-16T02:45:04.217 回答
1

I'm darned if I can find a reference at the moment, but I'm 99% sure that you are always supposed to be able to take the address of an argument, and it's up to the compiler to finesse the details of calling conventions, register usage, etc.

Indeed, I would have thought it to be such a common requirement that it's hard to see there can be general problem in this - I wonder if it's something about the volatile pointers which have upset the optimisation.

Personally, I might do try this to see if it compiled better:

void foo(unsigned int x) 
{
    volatile unsigned int* pArg = &x;
    *(volatile unsigned int*)(0x4000000 + 0xd4) = (unsigned int)pArg;
}
于 2008-08-26T17:14:17.697 回答
1

Tomi Kyöstilä写道

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

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

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

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

我认为您确实打算将颜色值存储在某个位置,直到 DMA 完成读取它为止。所以一个全局变量,或者一个函数局部静态变量,例如

// 警告:三星级程序员在工作

// 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”,因为我想强制以特定顺序写入两个值。

于 2011-03-15T19:33:27.247 回答
0

sparkes wrote

If you think you have found a bug in GCC the mailing lists will be glad you dropped by but generally they find some hole in your knowledge is to blame and mock mercilessly :(

I figured I'd try my luck here first before going to the GCC mailing list to show my incompetence :)


Adam Davis wrote

Out of curiosity, what are you trying to accomplish?

I was trying out development for the Game Boy Advance. I was reading about its DMA system and I experimented with it by creating single-color tile bitmaps. The idea was to have the indexed color be passed as an argument to a function which would use DMA to fill a tile with that color. The source address for the DMA transfer is stored at 0x40000d4.


Will Dean wrote

Personally, I might do try this to see if it compiled better:

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

With -O0 that works as well and with -O1 that is optimized to the exact same -O1 assembly I've posted in my question.

于 2008-08-26T17:54:48.083 回答
0

我认为 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);

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

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

于 2009-04-29T06:43:15.060 回答
0

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

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

于 2008-09-10T18:26:28.337 回答
0

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

于 2008-11-11T14:47:44.640 回答
-2

Is this just expected/undefined behaviour? Have I done something wrong or have I in fact found a Compiler Bug™?

No bug just the defined behaviour that optimisation options can produce odd code which might not work :)

EDIT:

If you think you have found a bug in GCC the mailing lists will be glad you dropped by but generally they find some hole in your knowledge is to blame and mock mercilessly :(

In this case I think it's probably the -O options attempting shortcuts that break your code that need working around.

于 2008-08-26T16:44:42.310 回答