是否在 C 中访问链接描述文件变量未定义行为的“值”?

Gab*_*les 5 c linker ld linker-scripts binutils

GNU ld(链接器脚本)手册第3.5.5源代码参考有一些关于如何访问 C 源代码中的链接器脚本“变量”(实际上只是整数地址)的非常重要的信息。我用了这个信息。广泛使用链接器脚本变量,我在这里写了这个答案:How to get value of variable defined in ld linker script from C

然而,很容易做错,并尝试访问链接描述文件变量的(错误地)而不是它的地址,因为这有点深奥。手册(上面的链接)说:

这意味着,你不能访问 链接脚本定义符号的-它没有价值-所有你能做的就是访问的地址链接脚本定义的符号。

因此,当您在源代码中使用链接描述文件定义的符号时,您应该始终获取该符号的地址,并且永远不要尝试使用其 value

问题:那么,如果您确实尝试访问链接描述文件变量的value,这是“未定义的行为”吗?

快速复习:

想象一下在链接脚本(例如:STM32F103RBTx_FLASH.ld)中你有:

/* Specify the memory areas */
MEMORY
{
    FLASH (rx)      : ORIGIN = 0x8000000,  LENGTH = 128K
    RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 20K
}

/* Some custom variables (addresses) I intend to access from my C source code */
__flash_start__ = ORIGIN(FLASH);
__flash_end__ = ORIGIN(FLASH) + LENGTH(FLASH);
__ram_start__ = ORIGIN(RAM);
__ram_end__ = ORIGIN(RAM) + LENGTH(RAM);
Run Code Online (Sandbox Code Playgroud)

在您的 C 源代码中,您执行以下操作:

// 1. correct way A:
extern uint32_t __flash_start__;
printf("__flash_start__ addr = 0x%lX\n", (uint32_t)&__flash_start__);

// OR 2. correct way B (my preferred approach):
extern uint32_t __flash_start__[]; // not a true array; [] is required to access linker script variables (addresses) as though they were normal variables
printf("__flash_start__ addr = 0x%lX\n", (uint32_t)__flash_start__);

// OR 3. COMPLETELY WRONG WAY TO DO IT!
// - IS THIS UNDEFINED BEHAVIOR?
extern uint32_t __flash_start__;
printf("__flash_start__ addr = 0x%lX\n", __flash_start__);
Run Code Online (Sandbox Code Playgroud)

示例打印输出

(这是真实的输出:它实际上是由 STM32 mcu 编译、运行和打印的):

  1. __flash_start__ addr = 0x8000000
  2. __flash_start__ addr = 0x8000000
  3. __flash_start__ addr = 0x20080000<== 注意就像我上面说的:这个是完全错误的(即使它编译并运行)!<== 2020 年 3 月更新:实际上,请参阅我的回答,这也很好,也很正确,只是做了一些不同的事情而已。

更新:

对@Eric Postpischil 的第一条评论的回应:

C 标准根本没有定义任何关于链接描述文件符号的内容。任何行为规范都取决于 GNU 工具。也就是说,如果链接描述文件符号标识了内存中存储一​​些有效对象的位置,我希望访问该对象的值能够工作,如果它是用正确的类型访问的。假设flash_start是通常可访问的内存,并且除了您的系统对flash_start 的内容的任何要求外,理论上您可以放置​​一个 uint32_t (使用适当的链接器输入),然后通过flash_start访问它。

是的,但这不是我的问题。我不确定你是否明白我的问题的微妙之处。看看我提供的例子。确实,您可以很好地访问此位置,但请确保您了解如何访问该位置,然后我的问题就会变得显而易见。请特别看上面的示例 3,即使对 C 程序员来说它看起来正确的,它也是错误的。要阅读 a ,例如, at ,您可以这样做:uint32_t__flash_start__

extern uint32_t __flash_start__;
uint32_t u32 = *((uint32_t *)&__flash_start__); // correct, even though it *looks like* you're taking the address (&) of an address (__flash_start__)
Run Code Online (Sandbox Code Playgroud)

或这个:

extern uint32_t __flash_start__[];
uint32_t u32 = *((uint32_t *)__flash_start__); // also correct, and my preferred way of doing it because it looks more correct to the trained "C-programmer" eye
Run Code Online (Sandbox Code Playgroud)

但绝对不是这个:

extern uint32_t __flash_start__;
uint32_t u32 = __flash_start__; // incorrect; <==UPDATE: THIS IS ALSO CORRECT! (and more straight-forward too, actually; see comment discussion under this question)
Run Code Online (Sandbox Code Playgroud)

而不是这个:

extern uint32_t __flash_start__;
uint32_t u32 = *((uint32_t *)__flash_start__); // incorrect, but *looks* right
Run Code Online (Sandbox Code Playgroud)

有关的:

  1. 为什么 STM32 gcc 链接器脚本会自动丢弃这些标准库中的所有输入部分:libc.a、libm.a、libgcc.a?
  2. [我的回答]如何从 C 获取 ld 链接描述文件中定义的变量的值

Gab*_*les 9

更简短的答案:

访问链接描述文件变量的“值”不是未定义的行为,只要您希望实际数据存储在内存中的该位置而不是该内存的地址或链接描述文件的“值”,就可以由C代码可以看出可变这恰好作为在存储器地址 值。

是的,这有点令人困惑,所以请仔细阅读 3 遍。本质上,如果您想访问链接描述文件变量的值,只需确保您的链接描述文件设置为防止您不想要的任何内容出现在该内存地址中,以便您想要的任何内容实际上都在那里。这样,读取该内存地址处的值将为您提供您期望的有用信息。

但是,如果您使用链接器脚本变量来存储某种“值”,那么在 C 中获取这些链接器脚本变量的“值”的方法是读取它们的地址,因为您的“值”分配给链接描述文件中的变量被 C 编译器视为该链接描述文件变量的“地址”,因为链接描述文件旨在操作内存和内存地址,而不是传统的 C 变量。

在我的问题下有一些非常有价值和正确的评论,我认为值得在这个答案中发布,这样他们就不会迷路了。请在我上面的问题下对他的评论进行投票。

C 标准根本没有定义任何关于链接描述文件符号的内容。任何行为规范都取决于 GNU 工具。也就是说,如果链接描述文件符号标识了内存中存储一​​些有效对象的位置,我希望访问该对象的值能够工作,如果它是用正确的类型访问的。假设__flash_start__是通常可访问的内存,除了您的系统对 at 的任何要求外__flash_start__,理论上您可以放置​​一个uint32_t(使用适当的链接器输入),然后通过__flash_start__.
– 埃里克·波斯特皮希尔

那个文档写得不是很好,你把第一句话写得太字面了。这里真正发生的是链接器的符号“值”概念和编程语言的标识符“值”概念是不同的东西。对于链接器来说,符号的值只是一个与之关联的数字。在编程语言中,值是存储在与标识符相关联的(有时是名义上的)存储中的数字(或某种类型的值集中的其他元素)。该文档建议您将符号的链接器值作为与标识符关联的地址出现在像 C 这样的语言中,而不是其存储的内容...

这部分非常重要,我们应该更新 GNU 链接器脚本手册:

当它告诉你“永远不要尝试使用它的价值”时,它走得太远了。

仅定义链接器符号不会为编程语言对象保留必要的存储空间是正确的,因此仅具有链接器符号并不能为您提供可以访问的存储空间。但是,如果确保通过其他方式分配存储空间,那么当然可以将其用作编程语言对象。如果您已正确分配存储并满足此要求,则不会普遍禁止将链接器符号用作 C 中的标识符,包括访问其 C 值。如果 的链接器值__flash_start__是有效的内存地址,并且您已确保uint32_t在该地址处有 a 的存储空间,并且它是 a 的正确对齐地址uint32_t,则可以访问__flash_start__在 C 中,就好像它是一个uint32_t. 这不是由 C 标准定义的,而是由 GNU 工具定义的。
– 埃里克·波斯皮希尔

长答案:

我在问题中说:

// 1. correct way A:
extern uint32_t __flash_start__;
printf("__flash_start__ addr = 0x%lX\n", (uint32_t)&__flash_start__);

// OR 2. correct way B (my preferred approach):
extern uint32_t __flash_start__[]; // not a true array; [] is required to access linker script variables (addresses) as though they were normal variables
printf("__flash_start__ addr = 0x%lX\n", (uint32_t)__flash_start__);

// OR 3. COMPLETELY WRONG WAY TO DO IT!
// - IS THIS UNDEFINED BEHAVIOR?
extern uint32_t __flash_start__;
printf("__flash_start__ addr = 0x%lX\n", __flash_start__);
Run Code Online (Sandbox Code Playgroud)

(请参阅问题下的讨论,了解我是如何想到这一点的)。

具体看上面的#3

嗯,其实,如果你的目标是读取地址__flash_start__,这是0x8000000在这种情况下,那么是的,这是完全错误的。但是,这不是未定义的行为!相反,它实际上在做的是读取该地址 ( )的内容(值0x8000000)作为uint32_t类型。换句话说,它只是读取 FLASH 部分的前 4 个字节,并将它们解释为uint32_t. 该内容uint32_t在该地址的值),只是这么恰巧是0x20080000在这种情况下。

为了进一步证明这一点,以下内容完全相同:

// Read the actual *contents* of the `__flash_start__` address as a 4-byte value!

// forward declaration to make a variable defined in the linker script
// accessible in the C code
extern uint32_t __flash_start__; 

// These 2 read techniques do the exact same thing.
uint32_t u32_1 = __flash_start__;                 // technique 1
uint32_t u32_2 = *((uint32_t *)&__flash_start__); // technique 2
printf("u32_1 = 0x%lX\n", u32_1);
printf("u32_2 = 0x%lX\n", u32_2);
Run Code Online (Sandbox Code Playgroud)

输出是:

u32_1 = 0x20080000
u32_2 = 0x20080000
Run Code Online (Sandbox Code Playgroud)

注意它们产生相同的结果。它们每个都产生一个有效的uint32_t类型值,该值存储在 address 0x8000000

然而,事实证明,u32_1上面显示的技术是一种更直接、更直接的读取值的方式,而且这不是未定义的行为。相反,它正在正确读取该地址的值(内容)。

我好像在兜圈子。无论如何,头脑都被炸了,但我现在明白了。在我应该只使用u32_2上面显示的技术之前,我被说服了,但事实证明它们都很好,而且,u32_1技术显然更直接(我又在兜圈子了)。:)

干杯。


深入挖掘:0x20080000存储在闪存开头的值从何而来?

再来一个小花絮。我实际上在 STM32F777 mcu 上运行了这个测试代码,它有 512KiB 的 RAM。由于 RAM 从地址 0x20000000 开始,这意味着 0x20000000 + 512K = 0x20080000。这恰好也是地址零处 RAM 的内容,因为编程手册 PM0253 修订版 4,第4页。42,“图 10. 向量表”显示向量表的前 4 个字节包含“初始 SP [堆栈指针] 值”。看这里:

在此处输入图片说明

我知道向量表位于程序存储器的开头,它位于闪存中,所以这意味着 0x20080000 是我的初始堆栈指针值。这是有道理的,因为Reset_Handler是程序的开始(顺便说一下,它的向量恰好是向量表开头的第二个 4 字节值),并且它做的第一件事,如我的“ startup_stm32f777xx.s ”启动程序集文件,将堆栈指针(sp)设置为_estack

Reset_Handler:  
  ldr   sp, =_estack      /* set stack pointer */
Run Code Online (Sandbox Code Playgroud)

此外,_estack在我的链接描述文件中定义如下:

/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM);    /* end of RAM */
Run Code Online (Sandbox Code Playgroud)

所以你有它!我的向量表中的第一个 4 字节值,就在 Flash 的开头,被设置为初始堆栈指针值,它_estack在我的链接描述文件中定义,并且_estack是我的 RAM 末尾的地址,即0x20000000 + 512K = 0x20080000。所以,这一切都说得通!我刚刚证明我读到了正确的价值!

也可以看看:

  1. [我的回答]如何从 C 获取 ld 链接器脚本中定义的变量的值