【问题标题】:Really Minimal STM32 Application: linker failure真正最小的 STM32 应用程序:链接器故障
【发布时间】:2018-04-17 18:24:59
【问题描述】:

我正在构建一个微型微控制器,其中仅包含用于自学目的的基本要素。这样,我可以刷新有关链接脚本、启动代码等主题的知识......


编辑:
我有很多 cmets 指出下面显示的“绝对最小的 STM32 应用程序”不好。当您注意到向量表不完整时,您是绝对正确的,.bss-section 没有处理,外围地址不完整,......请允许我解释原因。

  1. 在这一章中编写一个完整且有用的应用程序从来都不是作者的目的。他的目的是逐步解释链接脚本的工作原理、启动代码的工作原理、STM32 的引导过程是什么样的……纯粹是为了教育目的。我很欣赏这种方法,并且学到了很多东西。

  2. 我在下面给出的示例取自相关章节的中间部分。本章继续向链接描述文件和启动代码添加更多部分(例如.bss-section 的初始化)。
    我从他的章节中间把文件放在这里的原因是因为我遇到了一个特定的错误消息。我想在继续之前解决这个问题。

  3. 所讨论的章节在他书的结尾的某个地方。它适用于希望深入了解大多数人甚至不考虑的主题的更有经验或好奇的读者(大多数人使用制造商提供的标准链接描述文件和启动代码,但从未阅读过它)。

请记住这一点,请让我们专注于手头的技术问题(如下面的错误消息中所述)。也请接受我诚挚的歉意,我没有早先阐明作者的意图。但我现在已经完成了,所以我们可以继续;-)


 

1。绝对最小的STM32-应用程序

我正在学习的教程是本书的第 20 章:“掌握 STM32”(https://leanpub.com/mastering-stm32)。这本书解释了如何用两个文件制作一个微型微控制器应用程序:main.clinkerscript.ld。由于我没有使用 IDE(如 Eclipse),因此我还添加了 build.batclean.bat 来生成编译命令。所以我的项目文件夹是这样的:

在我继续之前,我或许应该提供一些关于我的系统的更多细节:

  • 操作系统:Windows 10,64 位

  • 微控制器:带有 STM32F401RE 微控制器的 NUCLEO-F401RE 板。

  • 编译器:arm-none-eabi-gcc 版本 6.3.1 20170620(发布)[ARM/embedded-6-branch 修订版 249437]。

主文件如下所示:

/* ------------------------------------------------------------ */
/*                     Minimal application                      */
/*                      for NUCLEO-F401RE                       */
/* ------------------------------------------------------------ */
typedef unsigned long uint32_t;

/* Memory and peripheral start addresses (common to all STM32 MCUs) */
#define FLASH_BASE      0x08000000
#define SRAM_BASE       0x20000000
#define PERIPH_BASE     0x40000000

/* Work out end of RAM address as initial stack pointer
 * (specific of a given STM32 MCU) */
#define SRAM_SIZE       96*1024 //STM32F401RE has 96 KB of RAM
#define SRAM_END        (SRAM_BASE + SRAM_SIZE)

/* RCC peripheral addresses applicable to GPIOA
 * (specific of a given STM32 MCU) */
#define RCC_BASE        (PERIPH_BASE + 0x23800)
#define RCC_APB1ENR     ((uint32_t*)(RCC_BASE + 0x30))

/* GPIOA peripheral addresses
 * (specific of a given STM32 MCU) */
#define GPIOA_BASE      (PERIPH_BASE + 0x20000)
#define GPIOA_MODER     ((uint32_t*)(GPIOA_BASE + 0x00))
#define GPIOA_ODR       ((uint32_t*)(GPIOA_BASE + 0x14))

/* Function headers */
int main(void);
void delay(uint32_t count);

/* Minimal vector table */
uint32_t *vector_table[] __attribute__((section(".isr_vector"))) = {
    (uint32_t*)SRAM_END,    // initial stack pointer (MSP)
    (uint32_t*)main         // main as Reset_Handler
};

/* Main function */
int main() {
    /* Enable clock on GPIOA peripheral */
    *RCC_APB1ENR = 0x1;

    /* Configure the PA5 as output pull-up */
    *GPIOA_MODER |= 0x400;  // Sets MODER[11:10] = 0x1

    while(1) {    // Always true
        *GPIOA_ODR = 0x20;
        delay(200000);
        *GPIOA_ODR = 0x0;
        delay(200000);
    }
}

void delay(uint32_t count) {
    while(count--);
}

 
链接描述文件如下所示:

/* ------------------------------------------------------------ */
/*                        Linkerscript                          */
/*                      for NUCLEO-F401RE                       */
/* ------------------------------------------------------------ */

/* Memory layout for STM32F401RE */
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
    SRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K
}

/* The ENTRY(..) directive overrides the default entry point symbol _start.
 * Here we define the main-routine as the entry point.
 * In fact, the ENTRY(..) directive is meaningless for embedded chips,
 * but it is informative for debuggers. */
ENTRY(main)

SECTIONS
{
    /* Program code into FLASH */
    .text : ALIGN(4)
    {
        *(.isr_vector)          /* Vector table */
        *(.text)                /* Program code */
        *(.text*)               /* Merge all .text.* sections inside the .text section */
        KEEP(*(.isr_vector))    /* Don't allow other tools to strip this off */
    } >FLASH


    _sidata = LOADADDR(.data);  /* Used by startup code to initialize data */

    .data : ALIGN(4)
    {
        . = ALIGN(4);
        _sdata = .;             /* Create a global symbol at data start */

        *(.data)
        *(.data*)

        . = ALIGN(4);
        _edata = .;             /* Define a global symbol at data end */
    } >SRAM AT >FLASH

}

 
build.bat 文件调用 main.c 上的编译器,然后是链接器:

@echo off
setlocal EnableDelayedExpansion

echo.
echo ----------------------------------------------------------------
echo.             )\     ***************************
echo.   ( =_=_=_=^<  ^|    * build NUCLEO-F401RE     *     
echo.             )(     ***************************
echo.             ""                        
echo.                                       
echo.
echo.   Call the compiler on main.c
echo.
@arm-none-eabi-gcc main.c -o main.o -c -MMD -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -O0 -g3 -Wall -fmessage-length=0 -Werror-implicit-function-declaration -Wno-comment -Wno-unused-function -ffunction-sections -fdata-sections
echo.
echo.   Call the linker
echo.
@arm-none-eabi-gcc main.o -o myApp.elf -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -specs=nosys.specs -specs=nano.specs -T linkerscript.ld -Wl,-Map=output.map -Wl,--gc-sections
echo.
echo.   Post build
echo.
@arm-none-eabi-objcopy -O binary myApp.elf myApp.bin
arm-none-eabi-size myApp.elf
echo.
echo ----------------------------------------------------------------

 
clean.bat 文件删除所有编译器输出:

@echo off
setlocal EnableDelayedExpansion

echo ----------------------------------------------------------------
echo.        __         **************    
echo.      __\ \___     *   clean    *    
echo.      \ _ _ _ \    **************    
echo.       \_`_`_`_\                     
echo.                                     
del /f /q main.o
del /f /q main.d
del /f /q myApp.bin
del /f /q myApp.elf
del /f /q output.map
echo ----------------------------------------------------------------

构建这个工程。我得到以下输出:

C:\Users\Kristof\myProject>build

----------------------------------------------------------------
             )\     ***************************
   ( =_=_=_=<  |    * build NUCLEO-F401RE     *
             )(     ***************************
             ""


   Call the compiler on main.c


   Call the linker


   Post build

   text    data     bss     dec     hex filename
    112       0       0     112      70 myApp.elf

----------------------------------------------------------------

 

2。正确的启动代码

也许您已经注意到最小应用程序没有正确的启动代码来初始化 .data 部分中的全局变量。 “掌握 STM32”一书中的 20.2.2 .data 和 .bss 部分初始化一章解释了如何执行此操作。

我的main.c 文件现在看起来像这样:

/* ------------------------------------------------------------ */
/*                     Minimal application                      */
/*                      for NUCLEO-F401RE                       */
/* ------------------------------------------------------------ */
typedef unsigned long uint32_t;

/* Memory and peripheral start addresses (common to all STM32 MCUs) */
#define FLASH_BASE      0x08000000
#define SRAM_BASE       0x20000000
#define PERIPH_BASE     0x40000000

/* Work out end of RAM address as initial stack pointer
 * (specific of a given STM32 MCU) */
#define SRAM_SIZE       96*1024 //STM32F401RE has 96 KB of RAM
#define SRAM_END        (SRAM_BASE + SRAM_SIZE)

/* RCC peripheral addresses applicable to GPIOA
 * (specific of a given STM32 MCU) */
#define RCC_BASE        (PERIPH_BASE + 0x23800)
#define RCC_APB1ENR     ((uint32_t*)(RCC_BASE + 0x30))

/* GPIOA peripheral addresses
 * (specific of a given STM32 MCU) */
#define GPIOA_BASE      (PERIPH_BASE + 0x20000)
#define GPIOA_MODER     ((uint32_t*)(GPIOA_BASE + 0x00))
#define GPIOA_ODR       ((uint32_t*)(GPIOA_BASE + 0x14))

/* Function headers */
void __initialize_data(uint32_t*, uint32_t*, uint32_t*);
void _start (void);
int main(void);
void delay(uint32_t count);

/* Minimal vector table */
uint32_t *vector_table[] __attribute__((section(".isr_vector"))) = {
    (uint32_t*)SRAM_END,    // initial stack pointer (MSP)
    (uint32_t*)_start       // _start as Reset_Handler
};

/* Variables defined in linkerscript */
extern uint32_t _sidata;
extern uint32_t _sdata;
extern uint32_t _edata;

volatile uint32_t dataVar = 0x3f;

/* Data initialization */
inline void __initialize_data(uint32_t* flash_begin, uint32_t* data_begin, uint32_t* data_end) {
    uint32_t *p = data_begin;
    while(p < data_end)
        *p++ = *flash_begin++;
}

/* Entry point */
void __attribute__((noreturn,weak)) _start (void) {
    __initialize_data(&_sidata, &_sdata, &_edata);
    main();

    for(;;);
}

/* Main function */
int main() {
    /* Enable clock on GPIOA peripheral */
    *RCC_APB1ENR = 0x1;

    /* Configure the PA5 as output pull-up */
    *GPIOA_MODER |= 0x400;  // Sets MODER[11:10] = 0x1

    while(dataVar == 0x3f) {    // Always true
        *GPIOA_ODR = 0x20;
        delay(200000);
        *GPIOA_ODR = 0x0;
        delay(200000);
    }
}

void delay(uint32_t count) {
    while(count--);
}

我在main(..) 函数上方添加了初始化代码。链接描述文件也有一些修改:

/* ------------------------------------------------------------ */
/*                        Linkerscript                          */
/*                      for NUCLEO-F401RE                       */
/* ------------------------------------------------------------ */

/* Memory layout for STM32F401RE */
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
    SRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K
}

/* The ENTRY(..) directive overrides the default entry point symbol _start.
 * In fact, the ENTRY(..) directive is meaningless for embedded chips,
 * but it is informative for debuggers. */
ENTRY(_start)

SECTIONS
{
    /* Program code into FLASH */
    .text : ALIGN(4)
    {
        *(.isr_vector)          /* Vector table */
        *(.text)                /* Program code */
        *(.text*)               /* Merge all .text.* sections inside the .text section */
        KEEP(*(.isr_vector))    /* Don't allow other tools to strip this off */
    } >FLASH


    _sidata = LOADADDR(.data);  /* Used by startup code to initialize data */

    .data : ALIGN(4)
    {
        . = ALIGN(4);
        _sdata = .;             /* Create a global symbol at data start */

        *(.data)
        *(.data*)

        . = ALIGN(4);
        _edata = .;             /* Define a global symbol at data end */
    } >SRAM AT >FLASH

}

这个小应用程序不再编译。其实从main.c编译到main.o还是可以的。但是链接过程卡住了:

C:\Users\Kristof\myProject>build

----------------------------------------------------------------
             )\     ***************************
   ( =_=_=_=<  |    * build NUCLEO-F401RE     *
             )(     ***************************
             ""


   Call the compiler on main.c


   Call the linker

c:/gnu_arm_embedded_toolchain/bin/../lib/gcc/arm-none-eabi/6.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/fpv4-sp/hard/crt0.o: In function `_start':
(.text+0x64): undefined reference to `__bss_start__'
c:/gnu_arm_embedded_toolchain/bin/../lib/gcc/arm-none-eabi/6.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/fpv4-sp/hard/crt0.o: In function `_start':
(.text+0x68): undefined reference to `__bss_end__'
collect2.exe: error: ld returned 1 exit status

   Post build

arm-none-eabi-objcopy: 'myApp.elf': No such file
arm-none-eabi-size: 'myApp.elf': No such file

----------------------------------------------------------------

 

3。我试过的

我已经省略了这部分,否则这个问题会变得太长;-)

 

4。解决方案

@berendi 提供了解决方案。谢谢@berendi!显然我需要将标志 -nostdlib-ffreestanding 添加到 gcc 和链接器。 build.bat 文件现在看起来像这样:

@echo off
setlocal EnableDelayedExpansion

echo.
echo ----------------------------------------------------------------
echo.             )\     ***************************
echo.   ( =_=_=_=^<  ^|    * build NUCLEO-F401RE     *     
echo.             )(     ***************************
echo.             ""                        
echo.                                       
echo.
echo.   Call the compiler on main.c
echo.
@arm-none-eabi-gcc main.c -o main.o -c -MMD -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -O0 -g3 -Wall -fmessage-length=0 -Werror-implicit-function-declaration -Wno-comment -Wno-unused-function -ffunction-sections -fdata-sections -ffreestanding -nostdlib
echo.
echo.   Call the linker
echo.
@arm-none-eabi-gcc main.o -o myApp.elf -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -specs=nosys.specs -specs=nano.specs -T linkerscript.ld -Wl,-Map=output.map -Wl,--gc-sections -ffreestanding -nostdlib
echo.
echo.   Post build
echo.
@arm-none-eabi-objcopy -O binary myApp.elf myApp.bin
arm-none-eabi-size myApp.elf
echo.
echo ----------------------------------------------------------------

现在可以了! 在他的回答中,@berendi 还对main.c 文件给出了一些有趣的评论。我已经应用了其中的大部分:

  1. 缺少volatile关键字

  2. 空循环

  3. Missing Memory Barrier(我是否将内存屏障放置在正确的位置?)

  4. 启用 RCC 后缺少延迟

  5. 具有误导性的符号名称(显然应该是 RCC_AHB1ENR 而不是 RCC_APB1ENR)。

  6. 向量表:这部分我已经跳过了。现在我真的不需要HardFault_HandlerMemManage_Handler,......因为这只是一个用于教育目的的小测试。
    尽管如此,我确实注意到@berendi 对他声明向量表的方式进行了一些有趣的修改。但我并没有完全理解他到底在做什么。

main.c 文件现在如下所示:

/* ------------------------------------------------------------ */
/*                     Minimal application                      */
/*                      for NUCLEO-F401RE                       */
/* ------------------------------------------------------------ */
typedef unsigned long uint32_t;

/**
  \brief   Data Synchronization Barrier
  \details Acts as a special kind of Data Memory Barrier.
           It completes when all explicit memory accesses before this instruction complete.
 */
__attribute__((always_inline)) static inline void __DSB(void)
{
  __asm volatile ("dsb 0xF":::"memory");
}


/* Memory and peripheral start addresses (common to all STM32 MCUs) */
#define FLASH_BASE      0x08000000
#define SRAM_BASE       0x20000000
#define PERIPH_BASE     0x40000000

/* Work out end of RAM address as initial stack pointer
 * (specific of a given STM32 MCU) */
#define SRAM_SIZE       96*1024 //STM32F401RE has 96 KB of RAM
#define SRAM_END        (SRAM_BASE + SRAM_SIZE)

/* RCC peripheral addresses applicable to GPIOA
 * (specific of a given STM32 MCU) */
#define RCC_BASE        (PERIPH_BASE + 0x23800)
#define RCC_AHB1ENR     ((volatile uint32_t*)(RCC_BASE + 0x30))

/* GPIOA peripheral addresses
 * (specific of a given STM32 MCU) */
#define GPIOA_BASE      (PERIPH_BASE + 0x20000)
#define GPIOA_MODER     ((volatile uint32_t*)(GPIOA_BASE + 0x00))
#define GPIOA_ODR       ((volatile uint32_t*)(GPIOA_BASE + 0x14))

/* Function headers */
void __initialize_data(uint32_t*, uint32_t*, uint32_t*);
void _start (void);
int main(void);
void delay(uint32_t count);

/* Minimal vector table */
uint32_t *vector_table[] __attribute__((section(".isr_vector"))) = {
    (uint32_t*)SRAM_END,    // initial stack pointer (MSP)
    (uint32_t*)_start       // _start as Reset_Handler
};

/* Variables defined in linkerscript */
extern uint32_t _sidata;
extern uint32_t _sdata;
extern uint32_t _edata;

volatile uint32_t dataVar = 0x3f;

/* Data initialization */
inline void __initialize_data(uint32_t* flash_begin, uint32_t* data_begin, uint32_t* data_end) {
    uint32_t *p = data_begin;
    while(p < data_end)
        *p++ = *flash_begin++;
}

/* Entry point */
void __attribute__((noreturn,weak)) _start (void) {
    __initialize_data(&_sidata, &_sdata, &_edata);
    asm volatile("":::"memory"); // <- Did I put this instruction at the right spot?
    main();

    for(;;);
}

/* Main function */
int main() {
    /* Enable clock on GPIOA peripheral */
    *RCC_AHB1ENR = 0x1;
    __DSB();

    /* Configure the PA5 as output pull-up */
    *GPIOA_MODER |= 0x400;  // Sets MODER[11:10] = 0x1

    while(dataVar == 0x3f) {    // Always true
        *GPIOA_ODR = 0x20;
        delay(200000);
        *GPIOA_ODR = 0x0;
        delay(200000);
    }
}

void delay(uint32_t count) {
    while(count--){
        asm volatile("");
    }
}

PS:Carmine Noviello 的《Mastering STM32》一书绝对是一部杰作。你应该读它! => https://leanpub.com/mastering-stm32

【问题讨论】:

  • 嗨@PeterJ_01。你是绝对正确的。我通常使用makefile。我在这里使用 bat 文件的原因是因为我只想编译 1 个文件并将其链接到可执行文件中。这是两个命令。我可以在 bat 文件中轻松做到这一点 :-)
  • 使用 CMSIS 定义 - 或手动使用数以万计的定义。不要从低级书籍中学习 ARM-s。当开始理解它时再回来。除非你真的需要,否则没有理由编写自己的启动文件。
  • 嗨@PeterJ_01,我明白你的意思。当然,我不会手写数千个定义。我也在正常项目中使用 CMSIS。但这只是一个用于(自我)教育目的的小项目。只用一个代码文件和一个小链接脚本就可以得到一个闪烁的 LED 是很有趣的。
  • 联系本书的作者,并在此处向他/她发送指向您问题的链接。如果我是作者,我肯定会帮助像你这样勤奋的读者!
  • 批处理文件,脚本,不相关。您没有为后一个示例显示您的构建命令吗?使用 gcc 链接只是感觉不对,但是如果没有它在其中获取 gcclib 的东西需要做更多的工作。您正在链接两个引导程序,这就是您遇到的问题。避免 .data 要求可以更轻松地管理裸机代码。同样假设 .bss 是零,无论如何都不应该在你写之前阅读,如果你这样做是一个坏习惯,工具现在开始警告这是一件好事。

标签: gcc arm microcontroller stm32 linker-scripts


【解决方案1】:

你可以告诉gcc不要使用这个库。

编译器

默认情况下,gcc 假定您使用的是标准 C 库,并且可以发出调用某些函数的代码。例如,启用优化时,它会检测到复制一段内存的循环,并可能用对memcpy() 的调用来替换它们。 使用-ffreestanding 禁用它。

链接器

链接器还假定您希望将程序与 C 库和启动代码链接起来。库启动代码负责初始化库和程序执行环境。它有一个名为_start() 的函数,必须在重置后调用。它的功能之一是用零填充.bss 段(见下文)。如果未定义分隔.bss 的符号,则无法链接_startup()。如果您将启动函数命名为其他任何名称,但 _startup(),那么库启动将被链接器作为未使用的函数悄悄删除,并且代码可能已被链接。

您可以告诉链接器不要使用 -nostdlib 链接任何标准库或启动代码,这样库提供的启动函数名称就不会与您的冲突,并且您每次都会收到链接器错误你不小心调用了一个库函数。

缺少volatile

您的寄存器定义缺少volatile 限定符。没有它,随后对*GPIOA_ODR 的写入将被优化。编译器会将这个“不变代码”移出循环。将寄存器定义中的类型更改为(volatile uint32_t*) 可以解决此问题。

空循环

优化器可以识别延迟循环什么都不做,并完全消除它以加快执行速度。在延迟循环中添加一个空但不可删除的asm volatile(""); 指令。

缺少内存屏障

您正在初始化 C 函数中包含 dataVar.data 部分。 __initialize_data() 中的*p 实际上是dataVar 的别名,编译器无从得知。优化器理论上可以在__initialize_data() 之前重新安排dataVar 的测试。即使dataVarvolatile*p 不是,因此无法保证订购。

在数据初始化循环之后,你应该告诉编译器程序变量被编译器未知的机制改变了:

asm volatile("":::"memory");

这是一个老式的 gcc 扩展,最新的 C 标准可能已经定义了一种可移植的方式来做到这一点(旧的 gcc 版本无法识别)。

启用 RCC 后缺少延迟

勘误表说,

为了管理外设对寄存器的读/写,应考虑 RCC 外设时钟启用和有效外设启用之间的延迟。

这个延迟取决于外设映射:

• 如果外设映射到 AHB:延迟应等于 2 个 AHB 周期。

• 如果外设映射到 APB:延迟应等于 1 + (AHB/APB 预分频器) 周期。

解决方法

  1. 使用 DSB 指令暂停 Cortex®-M4 CPU 流水线,直到指令完成。

因此,插入一个

__DSB();

*RCC_APB1ENR = 0x1; 之后(应该叫别的)

误导性符号名称

虽然RCC中启用GPIOA的地址似乎是正确的,但在文档中该寄存器被称为RCC_AHB1ENR。它会使试图理解您的代码的人感到困惑。

向量表

虽然从技术上讲,您可以在其中只使用一个堆栈定位器和一个重置处理程序,但我还是建议您多添加一些条目,至少是用于简单故障排除的故障处理程序。

__attribute__ ((section(".isr_vector"),used))
void (* const _vectors[]) (void) = {
          (void (*const)(void))(&__stack),
  Reset_Handler,
  NMI_Handler,
  HardFault_Handler,
  MemManage_Handler,
  BusFault_Handler,
  UsageFault_Handler
}

链接脚本

至少,它必须为您的向量表和代码定义一个部分。一个程序必须有一个起始地址和一些代码,静态数据是可选的。其余的取决于您的程序使用的数据类型。如果没有特定类型的数据,您可以从技术上将它们从链接描述文件中省略。

  • .rodata:只读数据,const 数组和结构都在这里。它们保持在闪存中。 (简单的const变量通常放在代码中)
  • .data:初始化变量,所有你声明的都带有= 符号,没有const
  • .bss:应该在 C 中初始化为零的变量,即全局变量和 static 变量。

因为你现在不需要.rodata.bss,没关系。

【讨论】:

  • 很好的答案!也非常感谢您提供的所有额外信息。我已经在编译和链接命令中添加了-nostdlib 标志,但我仍然得到同样的错误。这很奇怪......
  • 嗨@berendi,我尝试将标志-ffreestanding-nostdlib 添加到编译器,并将-Wl,-nostartfiles-Wl,-nostdlib-Wl,--ffreestanding 添加到链接器。但现在我收到另一条错误消息。请参阅我的问题底部的 EDIT 以获取更多详细信息。非常感谢您的帮助:-)
  • @K.Mulier 看起来链接器正在抱怨一个被识别为其他东西的选项。我会在我回到我的电脑时检查一下,如果你不知道的话。
  • 嗨@berendi。非常感谢你。最简单的检查方法是复制粘贴文件main.clinkerscript.ldbuild.batclean.bat。如果您遇到相同的错误消息,请运行构建并告诉我:-)
  • @K.Mulier 您不需要 -Wl 前缀,只需在第二次调用时将 -nostdlib -ffreestanding 传递给 gcc。你不需要-nostartfiles,因为-nostdlib 暗示了它。还要注意volatile 的其他问题以及我添加的延迟循环。
【解决方案2】:

一般来说,链接器脚本是一种艺术形式,它们是它们自己的编程语言,而 gnu 无疑是一场噩梦。将任务划分为从制作工作二进制文件中找出链接描述文件,一旦你可以看到链接描述文件正在做你想做的事情,然后让引导代码使用它。充分利用工具链。

作者使用的示例源自专门编写的代码,用作最大程度成功的裸机示例。避免了通用语言和工具链问题,但可以跨工具链的许多版本移植,并且可以轻松移植到其他工具链(对工具链的依赖最小,特别是导致引导程序的链接器脚本)。这本书的作者使用了该代码,但增加了它不作为示例可靠的风险。

在编写裸机代码时,专门避免使用 .data 并且不依赖 .bss 将其归零,这对取得长期成功大有帮助。

还对其进行了修改,以便优化会阻止该代码工作(以您可以看到的速度闪烁)。

一个用于 binutils 的最小链接器脚本示例,您可以对其进行修改以处理 .data 和 .bss 初始化,一般看起来像这样

test.ld

MEMORY
{
    bob : ORIGIN = 0x8000, LENGTH = 0x1000
    ted : ORIGIN = 0xA000, LENGTH = 0x1000
}

SECTIONS
{
   .text : { *(.text*) } > bob
   __data_rom_start__ = .;
   .data : {
    __data_start__ = .;
    *(.data*)
   } > ted AT > bob
   __data_end__ = .;
   __data_size__ = __data_end__ - __data_start__;
   .bss  : {
   __bss_start__ = .;
   *(.bss*)
   } > ted
   __bss_end__ = .;
   __bss_size__ = __bss_end__ - __bss_start__;
}

(注意内存名称不必是 rom 或 ram 或 flash 或 data 或任何 bob 是程序空间,而 ted 是内存 btw,根据需要更改地址)

您如何看待正在发生的事情是您可以链接一个简单的示例或您的代码,您需要一些 .data 和一些 .bss(以及一些 .text)。

向量.s

.thumb
.globl _start
_start:
.word 0x20001000
.word reset
.thumb_func
reset:
    bl notmain
    b .

.globl bss_start
bss_start: .word __bss_start__
.globl bss_end
bss_end: .word __bss_end__
.word __bss_size__
.globl data_rom_start
data_rom_start:
.word __data_rom_start__
.globl data_start
data_start:
.word __data_start__
.globl data_end
data_end:
.word __data_end__
.word __data_size__

so.c

unsigned int a=1;
unsigned int b=2;
unsigned int c;
unsigned int d;
unsigned int e;

unsigned int notmain ( void )
{
    return(a+b+c+d+e);
}

构建

arm-none-eabi-as vectors.s -o vectors.o
arm-none-eabi-gcc -O2 -c -mthumb so.c -o so.o
arm-none-eabi-ld -T test.ld vectors.o so.o -o vectors.elf
arm-none-eabi-objdump -D vectors.elf

到目前为止的代码并不特定于 arm-none-whatever 或 arm-linux-whatever 版本的工具链。如果/当您需要 gcclib 项目时,您可以使用 gcc 而不是 ld,但这样做时必须小心……或者提供 libgcc 的路径并使用 ld。

我们从这段代码中得到的是廉价的链接器脚本调试:

Disassembly of section .text:

00008000 <_start>:
    8000:   20001000    andcs   r1, r0, r0
    8004:   00008009    andeq   r8, r0, r9

00008008 <reset>:
    8008:   f000 f810   bl  802c <notmain>
    800c:   e7fe        b.n 800c <reset+0x4>

0000800e <bss_start>:
    800e:   0000a008    andeq   sl, r0, r8

00008012 <bss_end>:
    8012:   0000a014    andeq   sl, r0, r4, lsl r0
    8016:   0000000c    andeq   r0, r0, ip

0000801a <data_rom_start>:
    801a:   00008058    andeq   r8, r0, r8, asr r0

0000801e <data_start>:
    801e:   0000a000    andeq   sl, r0, r0

00008022 <data_end>:
    8022:   0000a008    andeq   sl, r0, r8
    8026:   00000008    andeq   r0, r0, r8
    ...

我们关心创建的 32 位值,andeq 反汇编是因为反汇编器试图将这些值反汇编为指令,而不是它们。复位指令是真实的,其余的是我们正在生成的 32 位值。也许可以使用readelf,但是习惯了反汇编,确保向量表正确作为第一步,这在反汇编中很容易看到。使用反汇编器作为一种习惯可以导致像上面那样使用它来向您展示链接器生成的内容。

如果您没有正确获取链接器脚本变量,您将无法编写成功的引导程序,如果您没有很好的方法来查看链接器生成的内容,您将经常失败。

是的,您可以在 C 中而不是汇编中公开它们,工具链仍然可以帮助您。

既然您可以看到链接器在做什么,您就可以朝着这个方向努力了:

.thumb
.globl _start
_start:
.word 0x20001000
.word reset
.thumb_func
reset:
    ldr r0,=__bss_start__
    ldr r1,=__bss_size__
    @ zero this

    ldr r0,=__data_rom_start__
    ldr r1,=__data_start__
    ldr r2,=__data_size__
    @ copy this

    bl notmain
    b .

给这样的东西

00008000 <_start>:
    8000:   20001000    andcs   r1, r0, r0
    8004:   00008009    andeq   r8, r0, r9

00008008 <reset>:
    8008:   4803        ldr r0, [pc, #12]   ; (8018 <reset+0x10>)
    800a:   4904        ldr r1, [pc, #16]   ; (801c <reset+0x14>)
    800c:   4804        ldr r0, [pc, #16]   ; (8020 <reset+0x18>)
    800e:   4905        ldr r1, [pc, #20]   ; (8024 <reset+0x1c>)
    8010:   4a05        ldr r2, [pc, #20]   ; (8028 <reset+0x20>)
    8012:   f000 f80b   bl  802c <notmain>
    8016:   e7fe        b.n 8016 <reset+0xe>
    8018:   0000a008    andeq   sl, r0, r8
    801c:   0000000c    andeq   r0, r0, ip
    8020:   00008058    andeq   r8, r0, r8, asr r0
    8024:   0000a000    andeq   sl, r0, r0
    8028:   00000008    andeq   r0, r0, r8

0000802c <notmain>:
    802c:   4b06        ldr r3, [pc, #24]   ; (8048 <notmain+0x1c>)
    802e:   6818        ldr r0, [r3, #0]
    8030:   685b        ldr r3, [r3, #4]
    8032:   18c0        adds    r0, r0, r3

如果然后对齐链接描述文件中的项目,则复制/零代码变得更加简单,您可以坚持 1 到某个数字 N 整个寄存器,而不是处理字节或半字,可以使用 ldr/str、ldrd/strd (如果可用)或 ldm/stm(不需要 ldrb/strb 也不需要 ldrh/strh),紧密简单的几行循环即可完成工作。

我强烈建议您不要在引导程序中使用 C。

请注意,ld 链接描述文件变量对位置非常敏感(大括号内或大括号外)

上面的链接描述文件有点典型,你会在股票链接描述文件中找到定义的开始和结束,有时在链接描述文件中计算大小,有时引导代码计算大小,或者引导代码可以循环直到地址等于最终值,取决于两者之间的整体系统设计。

顺便说一句,您的具体问题是您在两个引导程序中链接,在我写这篇文章时,我没有在问题中看到您的命令行,这样可以告诉我们更多信息。这就是为什么您会看到 bss_start 等,这些东西是您没有放入链接器脚本但通常在带有预构建工具链的库存中找到的(类似于上面,但更复杂)

这可能是通过使用 gcc 而不是 ld 并且没有各种 -nostartfiles 选项(它在 crt0.o 中拉入),只需尝试 ld 而不是 gcc 看看有什么变化。如果原来的例子是这样的,你会失败的,所以我认为这不是问题所在。如果您使用相同的命令行,则两个示例都应该失败,而不仅仅是后者。

【讨论】:

  • 一场噩梦?它是一种非常简单的定义语言。顺便说一句,我没有 DV。
  • 嗨@old_timer。非常感谢您的回答,以及您为此付出的所有努力!我很抱歉有人投了反对票 - 特别是因为那个人没有在评论中说明原因。
  • 最初的反对票来自我的前两份草稿,我放弃了,然后又重新加入并采取了不同的方法。这是非常罕见的,每两年左右一次我们会遇到一个像你一样真正有兴趣了解和学习的人,大多数人都想要喝酷儿的答案,而不在乎毒药(只希望鱼不想学习钓鱼)。 IMO 你似乎想学钓鱼,所以对你的成功有个人兴趣,必须有人知道这些东西是如何工作的,这样设备、编译器等才能在未来继续运行。
  • 请不要放弃baremetal,即使您只是为了好玩...这将有助于您在任何平台上使用任何语言进行的所有其他编程。
  • @old_timer arduino 生成 :(
【解决方案3】:

您正在阅读的书使您误入歧途。放弃它并开始从其他来源学习。

我发现它告诉您的操作至少有四个主要问题:

  1. 您包含的链接器脚本和_start 函数缺少许多重要部分,并且会出现故障或无法链接许多可执行文件。最值得注意的是,它缺乏对 BSS(零填充)部分的任何处理。

  2. main.c 中的向量表超出“最小”;它甚至缺少标准 ARM 中断向量的必需定义。没有这些,调试硬故障将变得非常困难,因为当故障发生时,微控制器会将向量表后面的随机代码视为中断向量,这可能会导致二次故障,因为它无法从该“地址”加载代码。

  3. 你书中给出的启动函数绕过了 libc 启动函数。这将导致标准 C 库的某些部分以及任何 C++ 代码无法正常工作。

  4. 您自己在main.c 中定义外设地址。这些地址都是在标准ST头文件中定义的(例如&lt;stm32f4xx.h&gt;),所以不需要自己定义。

作为初学者,我建议您参考 ST 在他们的任何示例中提供的启动代码。这些都将包括一个完整的链接器脚本和启动代码。

【讨论】:

  • 任何需要 C 库的东西都离最小或“绝对最小”还有很长的路要走。如果您注意到 .bss 被覆盖,需要知道在哪里查看(他们采用了两种方法,有点矫枉过正)。 ST 头文件也远非最小,并且带有一些沉重的包袱。所以请不要将 OP 引入更复杂、更不可能成功的道路。
  • @old_timer 1) 启用-Wl,-gc-sections 后,C 库与您拉入的部分一样大。对于最小的 Cortex-M 应用程序,最多只有几百字节,大部分在编译器预期的初始化代码中。
  • @old_timer 2) ST 头文件没有开销。它们只定义结构和宏——其中大部分与 OP 已经定义的相同或非常相似。你可能会想到 ST 外设库,这是一个单独的问题。
  • 您是否真的看过这些标题,而不是问题的大小。 C 库文件取决于库调用的大小,您提取的代码越多,成功的可能性就越小。对于所谓的绝对最小应用程序。
  • @old_timer 我对 ST 部分系列标头非常熟悉。再说一遍:它们是标题,而不是库。它们不包含代码,只包含声明。
【解决方案4】:

正如 old_timer 在 cmets 中所暗示的,使用 gcc 链接是一个问题。

如果您将批处理文件中的链接器调用更改为使用ld,则链接不会出错。请尝试以下操作:

echo.
echo.   Call the linker
echo.
@arm-none-eabi-ld main.o -o myApp.elf -T linkerscript.ld

【讨论】:

  • 嗨@DKrueger,谢谢你的建议。究竟有什么不同?我记得前段时间读到,与gcc 链接比与ld 链接更好,但我不记得为什么了。
  • @K.Mulier gcc is a frontend to ld,它确定需要链接哪些库(基于指令集选项、浮点等),并将它们的路径传递给 ld。在您的情况下,不需要这些库,您可以使用gcc -nostdlib 链接而不使用它们,或者直接调用ld
  • 正如@berendi 所说,gcc 会自动选择要添加到链接器的额外内容。这对于每次都需要将相同的项目传递给ld 的桌面应用程序很方便。但是您的项目不需要它们。正如您所注意到的,它会自动引入crt0,这会阻止您对_start 的实现进行链接。因此,与其在gcc 中添加一堆标志来告诉它排除额外内容,您可以简单地使用ld 并准确指定链接的内容。
  • 嗨@berendi 和DKrueger,非常感谢您澄清这一点:-)
猜你喜欢
  • 2018-04-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-19
  • 2014-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多