English | 简体中文
深入了解嵌入式软件中,在main函数之前,都需要做那些工作,以嘉立创天空星STM32F407VGT6为例。跟随youtube视频。
- 硬件初始化,会对内部的
pc、sp指针初始化。 - 系统初始化,比如配置时钟树等等。
- C运行(C Run Time 0)时,将已初始化的全局变量(.data)从 Flash 复制到 RAM,将未初始化的变量(.bss)清零。
- 构造函数(__libc_init_arry)。
- 调用
main函数。 while(1)死循环。
首先我们先编写一个最简单的程序main.c:
filename:main.c
int main(void)
{
return 0;
}然后运行下面的指令编译出一个.o文件
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -std=c11 -g -O0 -c main.c -o main.o我们使用arm-none-eabi-objdump查看它的反汇编:
arm-non-eabi-objdump -d main.o可以看到如下内容
main.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <main>:
0: b480 push {r7}
2: af00 add r7, sp, #0
4: 2300 movs r3, #0
6: 4618 mov r0, r3
8: 46bd mov sp, r7
a: bc80 pop {r7}
c: 4770 bx lr再使用这个main.o尝试编译生成一个.elf文件:
arm-none-eabi-gcc main.o -o main.elf会有如下的报错:
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a(libc_a-exit.o): in function `exit':
/build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/newlib/../../../newlib/libc/stdlib/exit.c:65:(.text+0x28): undefined reference to `_exit'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a(libc_a-writer.o): in function `_write_r':
/build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/newlib/../../../newlib/libc/reent/writer.c:49:(.text+0x24): undefined reference to `_write'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a(libc_a-sbrkr.o): in function `_sbrk_r':
/build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/newlib/../../../newlib/libc/reent/sbrkr.c:51:(.text+0x18): undefined reference to `_sbrk'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a(libc_a-closer.o): in function `_close_r':
/build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/newlib/../../../newlib/libc/reent/closer.c:47:(.text+0x18): undefined reference to `_close'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a(libc_a-readr.o): in function `_read_r':
/build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/newlib/../../../newlib/libc/reent/readr.c:49:(.text+0x24): undefined reference to `_read'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a(libc_a-lseekr.o): in function `_lseek_r':
/build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/newlib/../../../newlib/libc/reent/lseekr.c:49:(.text+0x24): undefined reference to `_lseek'
collect2: error: ld returned 1 exit status这是由于,即使我们只是简单的想要编译一个.c文件,gcc也会尝试去引入其他的一些组件,但是这些组件会干扰我们自定义启动流程。所以我们选择将其抛弃,但是在此之前,我们需要知道它都引入了什么。
首先,gcc会尝试链接newlib,这是一个面向嵌入式系统的C语言标准库实现(e.g. _exit, _write, _fork),这恰好对应了上述错误中的_exit等未定义的错误;我们之所以在日常使用HAL库不会报错,是因为ST公司的HAL库会提供一个syscall.c文件,来自己实现这些函数,所以在我们直接使用CubeMX生成的HAL库可以直接编译而不报错。syscall.c具体如下:
/**
******************************************************************************
* @file syscalls.c
* @author Auto-generated by STM32CubeMX
* @brief Minimal System calls file
*
* For more information about which c-functions
* need which of these lowlevel functions
* please consult the Newlib libc-manual
******************************************************************************
* @attention
*
* Copyright (c) 2020-2024 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* Includes */
#include <sys/stat.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <sys/times.h>
/* Variables */
extern int __io_putchar(int ch) __attribute__((weak));
extern int __io_getchar(void) __attribute__((weak));
char *__env[1] = { 0 };
char **environ = __env;
/* Functions */
void initialise_monitor_handles()
{
}
int _getpid(void)
{
return 1;
}
int _kill(int pid, int sig)
{
(void)pid;
(void)sig;
errno = EINVAL;
return -1;
}
void _exit (int status)
{
_kill(status, -1);
while (1) {} /* Make sure we hang here */
}
__attribute__((weak)) int _read(int file, char *ptr, int len)
{
(void)file;
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
*ptr++ = __io_getchar();
}
return len;
}
__attribute__((weak)) int _write(int file, char *ptr, int len)
{
(void)file;
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
__io_putchar(*ptr++);
}
return len;
}
int _close(int file)
{
(void)file;
return -1;
}
int _fstat(int file, struct stat *st)
{
(void)file;
st->st_mode = S_IFCHR;
return 0;
}
int _isatty(int file)
{
(void)file;
return 1;
}
int _lseek(int file, int ptr, int dir)
{
(void)file;
(void)ptr;
(void)dir;
return 0;
}
int _open(char *path, int flags, ...)
{
(void)path;
(void)flags;
/* Pretend like we always fail */
return -1;
}
int _wait(int *status)
{
(void)status;
errno = ECHILD;
return -1;
}
int _unlink(char *name)
{
(void)name;
errno = ENOENT;
return -1;
}
int _times(struct tms *buf)
{
(void)buf;
return -1;
}
int _stat(char *file, struct stat *st)
{
(void)file;
st->st_mode = S_IFCHR;
return 0;
}
int _link(char *old, char *new)
{
(void)old;
(void)new;
errno = EMLINK;
return -1;
}
int _fork(void)
{
errno = EAGAIN;
return -1;
}
int _execve(char *name, char **argv, char **env)
{
(void)name;
(void)argv;
(void)env;
errno = ENOMEM;
return -1;
}为了解决这个问题,我们可以在我们的编译指令中加上--specs=nosys.specs,在gcc中-specs=允许你在不修改gcc本身的情况下来自定义编译器的行为;我们这里使用的nosys.specs,代表只是简单引入_exit、_write等标准库函数,但是这些函数都是未实现的,只是简单的返回错误码,因为我们后续会自己尝试实现。
arm-none-eabi-gcc --specs=nosys.specs main.o -o main.elf运行这条命令,就可以看到,只有一些警告但是没有错误了,并且可以成功看到.elf输出:
❯ ll
.rw-rw-r-- 33 n1netynine99 25 1月 23:34 main.c
.rwxrwxr-x 105,336 n1netynine99 26 1月 14:05 main.elf
.rw-rw-r-- 1,884 n1netynine99 26 1月 13:36 main.o
.rw-rw-r-- 4,936 n1netynine99 26 1月 14:04 README.md我们可以使用下面的命令来查看这个.elf文件的大小:
arm-none-eabi-size main.elf输出可以看到,只是一个简单的C程序,加上一个链接文件,都占用了如下多的字节:
❯ arm-none-eabi-size main.elf
text data bss dec hex filename
7860 1364 808 10032 2730 main.elf这时候我们再尝试对其进行反汇编查看:
arm-none-eabi-objdump -d main.elf会发现他有特别多的输出,足足有两千多行。除了我们自己定义的main函数,其中还有很多的其他函数,比如memset,这是gcc在编译的时候自动链接的另一个库——Runtime Library(ctr*.o)。
下面是我截取反汇编中main函数中的部分:
0000824c <main>:
824c: b480 push {r7}
824e: af00 add r7, sp, #0
8250: 2300 movs r3, #0
8252: 4618 mov r0, r3
8254: 46bd mov sp, r7
8256: bc80 pop {r7}
8258: 4770 bx lr
/* 起始地址 机器码 汇编指令*/我们可以看到起始地址是从0x000 0824c开始的,这是一个32位的地址,因为我们指定的CPU类型-mcpu=cortex-m4就是一个32位的;但是通过我们查询STM32F4xx的数据手册,起始地址应该是0x0800 0000,这是因为我们没有提供一个链接文件.ld来指定相关的地址,所以gcc会默认使用一个默认的链接脚本;我们可以使用下面的指令来导出这个默认的链接脚本:
arm-none-eabi-gcc -specs=nosys.specs -Wl,--verbose main.o main.elf 在这行命令中,-Wl,的作用是,告诉gcc,把,之后的参数传给链接器而不是编译器;而--verbose这是让链接器输出详细的信息,这样我们就可以看到默认的链接脚本:
==================================================
/* Script for -z combreloc */
/* Copyright (C) 2014-2024 Free Software Foundation, Inc.
Copying and distribution of this script, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. */
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm",
"elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SEARCH_DIR("/usr/lib/arm-none-eabi/lib");
SECTIONS
{
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x8000)); . = SEGMENT_START("text-segment", 0x8000);
.interp : { *(.interp) }
.note.gnu.build-id : { *(.note.gnu.build-id) }
.hash : { *(.hash) }
.gnu.hash : { *(.gnu.hash) }
.dynsym : { *(.dynsym) }
.dynstr : { *(.dynstr) }
.gnu.version : { *(.gnu.version) }
.gnu.version_d : { *(.gnu.version_d) }
.gnu.version_r : { *(.gnu.version_r) }
.rel.dyn :
{
*(.rel.init)
*(.rel.text .rel.text.* .rel.gnu.linkonce.t.*)
*(.rel.fini)
*(.rel.rodata .rel.rodata.* .rel.gnu.linkonce.r.*)
*(.rel.data.rel.ro .rel.data.rel.ro.* .rel.gnu.linkonce.d.rel.ro.*)
*(.rel.data .rel.data.* .rel.gnu.linkonce.d.*)
*(.rel.tdata .rel.tdata.* .rel.gnu.linkonce.td.*)
*(.rel.tbss .rel.tbss.* .rel.gnu.linkonce.tb.*)
*(.rel.ctors)
*(.rel.dtors)
*(.rel.got)
*(.rel.bss .rel.bss.* .rel.gnu.linkonce.b.*)
PROVIDE_HIDDEN (__rel_iplt_start = .);
*(.rel.iplt)
PROVIDE_HIDDEN (__rel_iplt_end = .);
}
.rela.dyn :
{
*(.rela.init)
*(.rela.text .rela.text.* .rela.gnu.linkonce.t.*)
*(.rela.fini)
*(.rela.rodata .rela.rodata.* .rela.gnu.linkonce.r.*)
*(.rela.data .rela.data.* .rela.gnu.linkonce.d.*)
*(.rela.tdata .rela.tdata.* .rela.gnu.linkonce.td.*)
*(.rela.tbss .rela.tbss.* .rela.gnu.linkonce.tb.*)
*(.rela.ctors)
*(.rela.dtors)
*(.rela.got)
*(.rela.bss .rela.bss.* .rela.gnu.linkonce.b.*)
PROVIDE_HIDDEN (__rela_iplt_start = .);
*(.rela.iplt)
PROVIDE_HIDDEN (__rela_iplt_end = .);
}
.rel.plt :
{
*(.rel.plt)
}
.rela.plt :
{
*(.rela.plt)
}
.init :
{
KEEP (*(SORT_NONE(.init)))
}
.plt : { *(.plt) }
.iplt : { *(.iplt) }
.gnu.sgstubs : { *(.gnu.sgstubs*) }
.text :
{
*(.text.unlikely .text.*_unlikely .text.unlikely.*)
*(.text.exit .text.exit.*)
*(.text.startup .text.startup.*)
*(.text.hot .text.hot.*)
*(SORT(.text.sorted.*))
*(.text .stub .text.* .gnu.linkonce.t.*)
/* .gnu.warning sections are handled specially by elf.em. */
*(.gnu.warning)
*(.glue_7t) *(.glue_7) *(.vfp11_veneer) *(.v4_bx)
}
.fini :
{
KEEP (*(SORT_NONE(.fini)))
}
PROVIDE (__etext = .);
PROVIDE (_etext = .);
PROVIDE (etext = .);
.rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
.rodata1 : { *(.rodata1) }
.ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) }
.ARM.exidx :
{
PROVIDE_HIDDEN (__exidx_start = .);
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
PROVIDE_HIDDEN (__exidx_end = .);
}
.eh_frame_hdr : { *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) }
.eh_frame : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) }
.sframe : ONLY_IF_RO { *(.sframe) *(.sframe.*) }
.gcc_except_table : ONLY_IF_RO { *(.gcc_except_table .gcc_except_table.*) }
.gnu_extab : ONLY_IF_RO { *(.gnu_extab*) }
/* These sections are generated by the Sun/Oracle C++ compiler. */
.exception_ranges : ONLY_IF_RO { *(.exception_ranges*) }
/* Adjust the address for the data segment. We want to adjust up to
the same address within the page on the next page up. */
. = ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1));
/* Exception handling */
.eh_frame : ONLY_IF_RW { KEEP (*(.eh_frame)) *(.eh_frame.*) }
.sframe : ONLY_IF_RW { *(.sframe) *(.sframe.*) }
.gnu_extab : ONLY_IF_RW { *(.gnu_extab) }
.gcc_except_table : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) }
.exception_ranges : ONLY_IF_RW { *(.exception_ranges*) }
/* Thread Local Storage sections */
.tdata :
{
PROVIDE_HIDDEN (__tdata_start = .);
*(.tdata .tdata.* .gnu.linkonce.td.*)
}
.tbss : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) }
.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array))
PROVIDE_HIDDEN (__preinit_array_end = .);
}
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
PROVIDE_HIDDEN (__init_array_end = .);
}
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
PROVIDE_HIDDEN (__fini_array_end = .);
}
.ctors :
{
/* gcc uses crtbegin.o to find the start of
the constructors, so we make sure it is
first. Because this is a wildcard, it
doesn't matter if the user does not
actually link against crtbegin.o; the
linker won't look for a file to match a
wildcard. The wildcard also means that it
doesn't matter which directory crtbegin.o
is in. */
KEEP (*crtbegin.o(.ctors))
KEEP (*crtbegin?.o(.ctors))
/* We don't want to include the .ctor section from
the crtend.o file until after the sorted ctors.
The .ctor section from the crtend file contains the
end of ctors marker and it must be last */
KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
KEEP (*(SORT(.ctors.*)))
KEEP (*(.ctors))
}
.dtors :
{
KEEP (*crtbegin.o(.dtors))
KEEP (*crtbegin?.o(.dtors))
KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
KEEP (*(SORT(.dtors.*)))
KEEP (*(.dtors))
}
.jcr : { KEEP (*(.jcr)) }
.data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) }
.dynamic : { *(.dynamic) }
.got : { *(.got.plt) *(.igot.plt) *(.got) *(.igot) }
.data :
{
__data_start = .;
*(.data .data.* .gnu.linkonce.d.*)
SORT(CONSTRUCTORS)
}
.data1 : { *(.data1) }
_edata = .; PROVIDE (edata = .);
/* This section contains data that is initialized during load,
but not during the application's initialization sequence. */
.persistent : ALIGN(32 / 8)
{
PROVIDE (__persistent_start = .);
*(.persistent .persistent.* .gnu.linkonce.p.*)
. = ALIGN(32 / 8);
PROVIDE (__persistent_end = .);
}
. = ALIGN(ALIGNOF(NEXT_SECTION));
__bss_start = .;
__bss_start__ = .;
.bss :
{
*(.dynbss)
*(.bss .bss.* .gnu.linkonce.b.*)
*(COMMON)
/* Align here to ensure that the .bss section occupies space up to
_end. Align after .bss to ensure correct alignment even if the
.bss section disappears because there are no input sections.
FIXME: Why do we need it? When there is no .bss section, we do not
pad the .data section. */
. = ALIGN(. != 0 ? 32 / 8 : 1);
}
_bss_end__ = .; __bss_end__ = .;
/* This section contains data that is not initialized during load,
or during the application's initialization sequence. */
.noinit (NOLOAD) : ALIGN(32 / 8)
{
PROVIDE (__noinit_start = .);
*(.noinit .noinit.* .gnu.linkonce.n.*)
. = ALIGN(32 / 8);
PROVIDE (__noinit_end = .);
}
. = ALIGN(32 / 8);
. = SEGMENT_START("ldata-segment", .);
. = ALIGN(32 / 8);
__end__ = .;
_end = .; PROVIDE (end = .);
.stack 0x80000 :
{
_stack = .;
*(.stack)
}
/* Stabs debugging sections. */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
.comment 0 (INFO) : { *(.comment); LINKER_VERSION; }
.gnu.build.attributes : { *(.gnu.build.attributes .gnu.build.attributes.*) }
/* DWARF debug sections.
Symbols in the DWARF debugging sections are relative to the beginning
of the section so we begin them at 0. */
/* DWARF 1. */
.debug 0 : { *(.debug) }
.line 0 : { *(.line) }
/* GNU DWARF 1 extensions. */
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_sfnames 0 : { *(.debug_sfnames) }
/* DWARF 1.1 and DWARF 2. */
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
/* DWARF 2. */
.debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_line 0 : { *(.debug_line .debug_line.* .debug_line_end) }
.debug_frame 0 : { *(.debug_frame) }
.debug_str 0 : { *(.debug_str) }
.debug_loc 0 : { *(.debug_loc) }
.debug_macinfo 0 : { *(.debug_macinfo) }
/* SGI/MIPS DWARF 2 extensions. */
.debug_weaknames 0 : { *(.debug_weaknames) }
.debug_funcnames 0 : { *(.debug_funcnames) }
.debug_typenames 0 : { *(.debug_typenames) }
.debug_varnames 0 : { *(.debug_varnames) }
/* DWARF 3. */
.debug_pubtypes 0 : { *(.debug_pubtypes) }
.debug_ranges 0 : { *(.debug_ranges) }
/* DWARF 5. */
.debug_addr 0 : { *(.debug_addr) }
.debug_line_str 0 : { *(.debug_line_str) }
.debug_loclists 0 : { *(.debug_loclists) }
.debug_macro 0 : { *(.debug_macro) }
.debug_names 0 : { *(.debug_names) }
.debug_rnglists 0 : { *(.debug_rnglists) }
.debug_str_offsets 0 : { *(.debug_str_offsets) }
.debug_sup 0 : { *(.debug_sup) }
.ARM.attributes 0 : { KEEP (*(.ARM.attributes)) KEEP (*(.gnu.attributes)) }
.note.gnu.arm.ident 0 : { KEEP (*(.note.gnu.arm.ident)) }
/DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}
==================================================其中PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x8000)); . = SEGMENT_START("text-segment", 0x8000);就可以看出,它指定了起始地址是从0x8000开始。
由此可见,gcc默认的行为如下:
- 链接标准库(e.g. newlibc)
- 链接c运行时
- 默认的一个链接脚本
我们可以通过加上-nolibc来舍弃标准库,同时也可以删除--sepcs=nosys.specs:
arm-none-eabi-gcc -nolibc -Wl,--verbose main.o -o main.elf相较于之前那个命令,我们可以看到哦输出信息中我们已经少了下面这一段内容,说明libc.a已经被移除掉了:
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-memset.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-init.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-exit.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-atexit.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-fini.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-__call_atexit.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-__atexit.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-findfp.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-fwalk.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-impure.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-mallocr.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-freer.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-fclose.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-sysconf.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-stdio.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-writer.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-mlock.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-sbrkr.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-closer.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-fflush.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-errno.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-readr.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-lseekr.o
(/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/libc.a)libc_a-reent.o说明我们并没有链接这些内容,但是我们会发现有链接错误:
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/crt0.o: in function `_mainCRTStartup':
/build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/libgloss/../../../libgloss/arm/crt0.S:388:(.text+0xbc): undefined reference to `memset'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/libgloss/../../../libgloss/arm/crt0.S:538:(.text+0xfc): undefined reference to `atexit'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/libgloss/../../../libgloss/arm/crt0.S:540:(.text+0x100): undefined reference to `__libc_init_array'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/libgloss/../../../libgloss/arm/crt0.S:546:(.text+0x110): undefined reference to `exit'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /build/newlib-38V0JC/newlib-4.4.0.20231231/build/arm-none-eabi/libgloss/../../../libgloss/arm/crt0.S:546:(.text+0x12c): undefined reference to `__libc_fini_array'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: link errors found, deleting executable `main.elf'
collect2: error: ld returned 1 exit status这是因为我们的main.elf还是与默认的链接文件进行链接,但是又依赖于我们删除的代码;所以我们可以添加-nostartfiles来取消脚本文件的链接:
arm-none-eabi-gcc -nolibc -nostartfiles -Wl,--verbose main.o -o main.elf现在C运行时相关的库(crt*.o)也被删除了,我们只留下了libgcc.a,这是用来为gcc提供运行时支持的;从输出中可以看到,错误没有了,但是警告我们由于找不到_start入口标志,所以默认是0x0000 8000作为入口:
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: mode armelf
attempt to open main.o succeeded
main.o
attempt to open /usr/lib/gcc/arm-none-eabi/13.2.1/libgcc.so failed
attempt to open /usr/lib/gcc/arm-none-eabi/13.2.1/libgcc.a succeeded
/usr/lib/gcc/arm-none-eabi/13.2.1/libgcc.a
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: warning: cannot find entry symbol _start; defaulting to 00008000因此我们可以创建一个空白的.ld来替代他:
touch stm32f407vgt6.ld然后使用-T stm32f407vgt6.ld来链接他:
arm-none-eabi-gcc -nolibc -nostartfiles -T stm32f407vgt6.ld -Wl,--verbose main.o -o main.elf输出如下:
GNU ld (2.42-1ubuntu1+23) 2.42
Supported emulations:
armelf
opened script file stm32f407vgt6.ld
using external linker script:
==================================================
==================================================
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: mode armelf
attempt to open main.o succeeded
main.o
attempt to open /usr/lib/gcc/arm-none-eabi/13.2.1/libgcc.so failed
attempt to open /usr/lib/gcc/arm-none-eabi/13.2.1/libgcc.a succeeded
/usr/lib/gcc/arm-none-eabi/13.2.1/libgcc.a由于我们的链接文件是空的,所以这里using external linker script:部分也是空白的,我们再次对main.elf进行反汇编,我们可以看到和我们最初的并没有什么区别:
❯ arm-none-eabi-objdump -d main.elf
main.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <main>:
0: b480 push {r7}
2: af00 add r7, sp, #0
4: 2300 movs r3, #0
6: 4618 mov r0, r3
8: 46bd mov sp, r7
a: bc80 pop {r7}
c: 4770 bx lr并且使用arm-none-eabi-size main.elf可以看到只占用14个字节:
text data bss dec hex filename
14 0 0 14 e main.elfCaution
不同的gcc版本可能大小有差异,这里我的版本是13.3.0
有了以上内容,我们现在可以开始实现自己启动流程了。
首先,CortexM4上电后,硬件会让sp从地址0x0000 0000读取一个 32 位的值,直接装入MSP主堆栈指针);pc指针会从地址 0x0000 0004 读取一个 32 位的值,作为Reset_Handler复位向量)的入口地址装入 pc;但是我们这里还是用软件描述一次,通常这些步骤都会在startup.s中使用汇编实现,但是我们这里就用C语言实现,我称之为startup_stm32f407vgt6.c。
首先,我们需要定义一个复位函数,也就是 Reset_Handler,在这里我给他命名为ISR_ResetHandler,这个函数就是上电复位后直接进行的函数,我们的主函数也是在这里被调用的,所以如下:
filename:startup_stm32f407vgt6.c
extern int main(void);
void ISR_ResetHandler(void)
{
main();
// 理论上永远不会退出main函数
while(1);
}我们还需要一个错误处理函数,在这里我们就简单的使用一个死循环:
filename:startup_stm32f407vgt6.c
extern int main(void);
void ISR_ResetHandler(void)
{
main();
// 理论上永远不会退出main函数
while (1);
}
void ISR_HardFaultHandler(void)
{
while (1);
}接下来,我们需要定义中断向量表,根据官方手册,STM32F407总共有92个中断向量需要定义:内核异常 16 个、外部中断82 个,并且我们需要按照手册中的顺序排列,不过由于这只是个学习的demo,我们只保留几个关键的定义,其余全部保持为0:
filename:startup_stm32f407vgt6.c
#define STM32F407XX_IVT_AMOUNT (96)
typedef void (*isr_t)(void); // 定义一个中断向量的数据类型
extern unsigned int _stack; // 在 .ld 中定义
extern int main(void);
void ISR_ResetHandler(void)
{
main();
// 理论上永远不会退出main函数
while (1);
}
void ISR_HardFaultHandler(void)
{
while (1);
}
// 使用 used 避免编译器将其优化
// 需要在.ld中定义 .ivt 段
__attribute((used, section(".ivt"))) static const isr_t ivt_table[STM32F407XX_IVT_AMOUNT] = {
(isr_t)(&_stack),
ISR_ResetHandler,
0, // NMI
ISR_HardFaultHandler,
// 由于只是演示demo,剩余的向量都设置为0
// 实际工程中可以设置为弱定义函数
};接下来我们需要编写.ld文件,具体语法这里不多说,我根据官方的手册编写的如下:
filename:stm32f407vgt6.ld
MEMORY
{
flash(rx) : ORIGIN = 0x08000000,LENGTH = 1024K
ram(rwx) : ORIGIN = 0x10000000,LENGTH = 64K
}
_eram = ORIGIN(ram) + LENGTH(ram); /* ram的边界 */
_stack = _eram; /* 栈从ram的末尾开始,并且向下增长 */
SECTIONS
{
.text : ALIGN(4)
{
KEEP(*(.ivt)) /* 存放的是中断向量表 */
*(.text*) /* 存放所有以text开头的段 */
. = ALIGN(4);
_etext = .; /* 用于标记text段的末尾 */
} > flash
}现在我们可以编译我们的启动文件startup_stm32f407vgt6.c,我们不需要重新编译我们的main.c,因为并没有做修改:
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -std=c11 -g -O0 -c startup_stm32f407vgt6.c -o startup_stm32f407vgt6.o现在我们有了两个.o文件和一个.ld文件:
❯ ll
.rw-rw-r-- 33 n1netynine99 25 1月 23:34 main.c
.rwxrwxr-x 5,516 n1netynine99 26 1月 15:16 main.elf
.rw-rw-r-- 1,884 n1netynine99 26 1月 13:36 main.o
.rw-rw-r-- 30,904 n1netynine99 26 1月 20:07 README.md
.rw-rw-r-- 714 n1netynine99 26 1月 20:07 startup_stm32f407vgt6.c
.rw-rw-r-- 3,076 n1netynine99 26 1月 20:05 startup_stm32f407vgt6.o
.rw-rw-r-- 454 n1netynine99 26 1月 20:00 stm32f407vgt6.ld然后可以使用这两个.o文件和这个.ld文件来链接生成新的.elf文件:
arm-none-eabi-gcc -nolibc -nostartfiles -T stm32f407vgt6.ld -Wl,--verbose main.o startup_stm32f407vgt6.o -o main.elf输出如下:
GNU ld (2.42-1ubuntu1+23) 2.42
Supported emulations:
armelf
opened script file stm32f407vgt6.ld
using external linker script:
==================================================
MEMORY
{
flash(rx) : ORIGIN = 0x08000000,LENGTH = 1024K
ram(rwx) : ORIGIN = 0x10000000,LENGTH = 64K
}
_eram = ORIGIN(ram) + LENGTH(ram); /* ram的边界 */
_stack = _eram; /* 栈从ram的末尾开始,并且向下增长 */
SECTIONS
{
.text : ALIGN(4)
{
KEEP(*(.ivt)) /* 存放的是中断向量表 */
*(.text*) /* 存放所有以text开头的段 */
. = ALIGN(4);
_etext = .; /* 用于标记text段的末尾 */
} > flash
}
==================================================
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: mode armelf
attempt to open main.o succeeded
main.o
attempt to open startup_stm32f407vgt6.o succeeded
startup_stm32f407vgt6.o
attempt to open /usr/lib/gcc/arm-none-eabi/13.2.1/libgcc.so failed
attempt to open /usr/lib/gcc/arm-none-eabi/13.2.1/libgcc.a succeeded
/usr/lib/gcc/arm-none-eabi/13.2.1/libgcc.a我们可以使用反汇编来查看存储是否按照我们所想的位置存放的:
arm-none-eabi-objdump -D main.elf可以从下面的输出看到,我们的中断向量表确实是定义到了0x08000 0000的位置:
Disassembly of section .text:
08000000 <ivt_table>:
8000000: 00010000 andeq r0, r1, r0
8000004: 0800018f stmdaeq r0, {r0, r1, r2, r3, r7, r8}
8000008: 00000000 andeq r0, r0, r0
800000c: 08000199 stmdaeq r0, {r0, r3, r4, r7, r8} 接下来我们可以debug看一下是否正常,将开发板连接之后,使用vscode的cortex debug插件进行调试,具体配置如下:
filename:launch.json
{
"version": "0.2.0",
"configurations": [
{
"cwd": "${workspaceRoot}",
"executable": "${workspaceFolder}/main.elf",
"name": "Debug with OpenOCD",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"configFiles": [
"interface/stlink.cfg",
"target/stm32f4x.cfg"
],
"searchDir": [],
// 上电就复位
"breakAfterReset": true,
// "runToEntryPoint": "main",
"showDevDebugOutput": "none"
}
]
}复位之后我们观察寄存器的值,可以发现pc指针刚好指向ISR_ResetHandler;而sp指针也是指向我们栈的开头,也就是我们在.ld中对应的_stack的位置再向下2个字节(向下增长):
但是还没有结束,因为我们只是完成了硬件的初始化,还不正常运行C语言,因为我们还未实现C语言环境的搭建,我们可以向我们的main.c中添加一些内容,并在调试的时候查看是否能够安装我们预期进行:
filename:main.c
int global_var = 10; // 初始化了的全局变量
int uninit_global_var; // 未初始化的全局变量 按照C标准应该初始化为0
int main(void)
{
int local_var = 20; // 初始化了的局部变量
static int static_local_var = 30; // 初始化的静态变量
// 未初始化的静态变量 按照C标准应该初始化为0
static int uninit_static_local_var;
static int sum;
sum = global_var + uninit_global_var + local_var + static_local_var + uninit_static_local_var;
if (sum == 60)
{
while (1);
}
else
{
while (1);
}
return 0;
}重新编译然后调试,观察结果:
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -std=c11 -g -O0 -c main.c -o main.o
arm-none-eabi-gcc -nolibc -nostartfiles -T stm32f407vgt6.ld -Wl,--verbose main.o startup_stm32f407vgt6.o -o main.elf调试可以发现,最后是到了else分支的死循环中,并且对应的值都不对,sum的值也没有更新:
反汇编查看对应的地方可以看到:
这里有一句str r3, [r2, #0],其实就是将r3中的值存储到r2存储的值所指向的内存,#0代表偏移量是0;我们可以看到r3此时的值正是当前情况下计算得到的值58,但是r2的值是134218216,对应的16进制为0x0800 01e8,对应的是flash地址,是只读的,所以我们的sum并没有更新。而之所以是58是因为,.bss段的数据没有初始化之前都会被一个标志位1标记,如果用int去看待,那他得到的数就为-1。
Note
这里不会产生硬件错误从而进入我们的ISR_HardFaultHandler,因为在很多 STM32 芯片的默认配置下,直接向 Flash 区域执行写操作(STR),如果 Flash 接口没有解锁或使能编程模式,硬件并不会产生 BusFault 或 HardFault
为了解决这个问题,我们需要在.ld文件中新增.data段,将其置于ram中,并且使用AT关键字告诉链接器,初始化的时候从flash中复制初始值;还要添加.bss段,用于存放初始化为0的变量,比如这里的uninit_global_var,在上电的时候, .bss段的所有内容都会被清0,所以在C语言中没有初始化的静态变量和局部变量的初始值都是0。
将下列代码添加到stm32f407vgt6.ld中:
/* 使用_idata来引用data段 */
_idata = LOADADDR(.data);
.data : ALIGN(4)
{
_data = .;
*(.data*)
. = ALIGN(4);
_edata = .;
} > ram AT > flash
.bss : ALIGN(4)
{
_bss = .;
*(.bss*)
. = ALIGN(4);
_ebss = .;
PROVIDE(_end = .);/* PROVIDE关键字就是若定义,没人定义这个 _end 就用这个定义,指示了堆从哪里开始 */
} > ram
重新编译main.elf,然后再次调试:
会发现,类加值还是不对,并且这次连全局变量global_var的值也不对了;这是由于我们需要在进入main函数之间就对ram进行初始化,因为刚刚上电的时候ram的值都是垃圾值。
所以我们需要在ISR_ResetHandler中执行.s类似的操作:
- 将
.data段的数据从flash中搬运到ram中,因为flash是只读的。 - 将
.bss段所对应的区域全部初始化为0
现在我们的startup_stm32f407vgt6.c是下面这样:
filename:startup_stm32f407vgt6.c
#define STM32F407XX_IVT_AMOUNT (96)
typedef void (*isr_t)(void); // 定义一个中断向量的数据类型
// 在 .ld 中定义
extern unsigned int _stack;
extern unsigned int _idata;
extern unsigned int _data;
extern unsigned int _edata;
extern unsigned int _bss;
extern unsigned int _ebss;
// 主函数
extern int main(void);
// 将 none-zero-init 的全局/静态变量从 flash 搬运到 ram 中
static void copy_data_from_flash_to_ram(void)
{
unsigned int *src_ptr = &_idata; // _idata 是我们想要复制的数据的 flash 的起始地址
unsigned int *dst_ptr = &_data; // _data 是搬运到 ram 的目标地址
// 一直搬运直到达到了 .data 段的末尾
while (dst_ptr < &_edata)
{
*dst_ptr++ = *src_ptr++;
}
}
// 将 zero-init 的全局/静态变量全部设置为0
static void clear_bss_section(void)
{
unsigned int *bss_ptr = &_bss; // _bss 是 bss 段的开始位置
// 直到 bss 段的末尾
while (bss_ptr < &_ebss)
{
*bss_ptr++ = 0;
}
}
void ISR_ResetHandler(void)
{
// 在进入 main 函数之前调用
copy_data_from_flash_to_ram();
clear_bss_section();
main();
// 理论上永远不会退出 main 函数
while (1);
}
void ISR_HardFaultHandler(void)
{
while (1);
}
// 使用 used 避免编译器将其优化
// 需要在 .ld 中定义 .ivt 段
__attribute((used, section(".ivt"))) static const isr_t ivt_table[STM32F407XX_IVT_AMOUNT] = {
(isr_t)(&_stack),
ISR_ResetHandler,
0, // NMI
ISR_HardFaultHandler,
// 由于只是演示demo,剩余的向量都设置为0
// 实际工程中可以设置为弱定义函数
};然后重新编译这个文件,并且重新生成.elf文件:
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -std=c11 -g -O0 -c stm32f407vgt6.ld startup_stm32f407vgt6.c -o startup_stm32f407vgt6.o
arm-none-eabi-gcc -nolibc -nostartfiles -T stm32f407vgt6.ld -Wl,--verbose main.o startup_stm32f407vgt6.o -o main.elf 现在再调试,可以看到程序完美的按照我们的预期进行了!
总结一下我们在进入main之前都需要做哪些工作:
首先是startup文件:
- 我们需要定义好中断向量表,将他放到指定的位置
- 其次是一个复位函数
ISR_ResetHandler,他需要完成:- 将初始值非零的全局和静态变量(.data)从
flash复制到ram中 - 将初始值为0的区域(.bss)全部设置为0
- 调用主函数
- 一个死循环,避免主函数退出
- 将初始值非零的全局和静态变量(.data)从
然后是链接脚本的部分:
- 定义
MEMORY,这是整个系统的内存布局 - 定义
SECTION,这是不同段在内存中的具体分布
有了上述两个部分,我们就可以完成一个C语言最低的运行要求。接下来,我们可以重新链接标准库,也就是删除-nolibc这个部分,来链接一个-specs=nano.specs,这是一个更小的标准库,然后指定CPU类型和指令集:
arm-none-eabi-gcc -specs=nano.specs -mcpu=cortex-m4 -mthumb -nostartfiles -T stm32f407vgt6.ld -Wl,--verbose main.o startup_stm32f407vgt6.o -o main.elf现在我们就可以使用一些常见的函数,比如memcpy、memeset等:
#include <string.h>
int global_var = 10; // 初始化了的全局变量
int uninit_global_var; // 未初始化的全局变量 按照C标准应该初始化为0
char arry[10];
int main(void)
{
int local_var = 20; // 初始化了的局部变量
static int static_local_var = 30; // 初始化的静态变量
// 未初始化的静态变量 按照C标准应该初始化为0
static int uninit_static_local_var;
static int sum;
sum = global_var + uninit_global_var + local_var + static_local_var + uninit_static_local_var;
memset(arry, 99, sizeof(arry));
if (sum == 60)
{
while (1);
}
else
{
while (1);
}
return 0;
}按照我们之前的-nolibc编译(直接使用.c编译,不再单独编译.o文件)
arm-none-eabi-gcc -nolibc -nostartfiles -T stm32f407vgt6.ld -Wl,--verbose main.c startup_stm32f407vgt6.c -o main.elf可以看到,编译会报错找不到memset:
main.c:(.text+0x5c): undefined reference to `memset'
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: link errors found, deleting executable `main.elf'
collect2: error: ld returned 1 exit status但是我们用我们链接好的命令:
arm-none-eabi-gcc -specs=nano.specs -mcpu=cortex-m4 -g -O0 -mthumb -nostartfiles -T stm32f407vgt6.ld -Wl,--verbose main.c startup_stm32f407vgt6.c -o main.elf就可以成功编译,并且调试的时候也可以正常看到arry的所有值都被设置为99:
紧接着我们可以让我们的startup文件更加完整,把硬件初始化和构造函数初始化也加上:
filename:startup_stm32f407vgt6.c
#define STM32F407XX_IVT_AMOUNT (96)
typedef void (*isr_t)(void); // 定义一个中断向量的数据类型
// 在 .ld 中定义
extern unsigned int _stack;
extern unsigned int _idata;
extern unsigned int _data;
extern unsigned int _edata;
extern unsigned int _bss;
extern unsigned int _ebss;
// 主函数
extern int main(void);
// 构造函数初始化 用 Newlibc 提供 (我们这里是 nano 版本)
extern void __libc_init_array(void);
// 将 none-zero-init 的全局/静态变量从 flash 搬运到 ram 中
static void copy_data_from_flash_to_ram(void)
{
unsigned int *src_ptr = &_idata; // _idata 是我们想要复制的数据的 flash 的起始地址
unsigned int *dst_ptr = &_data; // _data 是搬运到 ram 的目标地址
// 一直搬运直到达到了 .data 段的末尾
while (dst_ptr < &_edata)
{
*dst_ptr++ = *src_ptr++;
}
}
// 将 zero-init 的全局/静态变量全部设置为0
static void clear_bss_section(void)
{
unsigned int *bss_ptr = &_bss; // _bss 是 bss 段的开始位置
// 直到 bss 段的末尾
while (bss_ptr < &_ebss)
{
*bss_ptr++ = 0;
}
}
// 硬件初始化,配置时钟等
static void system_init(void)
{
}
void ISR_ResetHandler(void)
{
// 硬件初始化
system_init();
// 在进入 main 函数之前调用
copy_data_from_flash_to_ram();
clear_bss_section();
// 初始化构造函数
__libc_init_array();
main();
// 理论上永远不会退出 main 函数
while (1);
}
void ISR_HardFaultHandler(void)
{
while (1);
}
// 使用 used 避免编译器将其优化
// 需要在 .ld 中定义 .ivt 段
__attribute((used, section(".ivt"))) static const isr_t ivt_table[STM32F407XX_IVT_AMOUNT] = {
(isr_t)(&_stack),
ISR_ResetHandler,
0, // NMI
ISR_HardFaultHandler,
// 由于只是演示demo,剩余的向量都设置为0
// 实际工程中可以设置为弱定义函数
};但是现在还不可以直接编译:
/usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/bin/ld: /usr/lib/gcc/arm-none-eabi/13.2.1/../../../arm-none-eabi/lib/thumb/v7e-m/nofp/libc_nano.a(libc_a-init.o): in function `__libc_init_array':
/build/newlib-38V0JC/newlib-4.4.0.20231231/build_nano/arm-none-eabi/thumb/v7e-m/nofp/newlib/../../../../../../newlib/libc/misc/init.c:40:(.text+0x1e): undefined reference to `_init'因为__lib_init_array会调用一个_init,通常来说这个_init函数是由crti.s提供,但是我们添加了-nostartfiles,所以不会有这个函数,这会让链接器找不到该函数的定义,解决方法也很简单,在startup_stm32f407vgt6.c中加上下面这个空白占位符:
void _init(void)
{
}同时,我们还需要在 .ld中添加下列内容:
.init_array : ALIGN(4)
{
__init_array_start = .;
KEEP(*(.init_array*))
__init_array_end = .;
. = ALIGN(4);
} > flash再次编译得到的.elf,调试发现,调用__libc_init_array的时候,会调用_init函数,并且所有加上__attribute((constructor))的都会在指定的位置放置。
现在我们就完成了所有的基础配置!最后为了更加优雅,我们将上述的所有依赖都转成CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
# 设置工具链
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_PROJECT_NAME main)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# 不生成可执行文件
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
project(${CMAKE_PROJECT_NAME} C ASM CXX)
set(M4_FLAGS
-mcpu=cortex-m4
-mthumb
# -mfpu=fpv4-sp-d16
# -mfloat-abi=hard
)
set(DBG_FLAGS
-Wall
-O0
-g
)
add_compile_options(${M4_FLAGS})
add_compile_options(${DBG_FLAGS})
add_link_options(${M4_FLAGS})
add_link_options(
-nostartfiles
-T${CMAKE_SOURCE_DIR}/stm32f407vgt6.ld
--specs=nano.specs
-Wl,--verbose
)
add_executable(${CMAKE_PROJECT_NAME}.elf
${CMAKE_SOURCE_DIR}/main.c
${CMAKE_SOURCE_DIR}/startup_stm32f407vgt6.c
)
之后就可以使用下列的指令来编译生成.elf文件:
cmake -B build -G Ninja
ninja -C build




