diff --git a/README.md b/README.md index 236748b..43be759 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ $ libxr stm32 setup -t usart1 [INFO] System: FreeRTOS [INFO] Generated User: wrote app_main.cpp, app_main.h, flash_map.hpp, libxr_config.yaml [INFO] Generated LibXR.CMake at: cmake\LibXR.CMake +[INFO] Placed the GOT in FLASH after .fini_array in STM32F103xx_FLASH.ld [INFO] LibXR.CMake included in CMakeLists.txt. [INFO] [Pass] All tasks completed. [INFO] Next: #include "app_main.h" and call app_main() in the default task StartDefaultTask (Core/Src/main.c), inside USER CODE sections, which CubeMX keeps when it regenerates the code. @@ -637,6 +638,29 @@ $ libxr stm32 toolchain clang --newlib [INFO] Done. ``` +`libxr stm32 cmake` 还处理工程根目录中的链接脚本(`*.ld`)。CubeMX 写出的链接脚本没有 `.got` +输出段,而 CubeMX 生成的 `syscalls.c` 和 `sysmem.c` 会写 `errno`;ST Arm Clang 的 picolibc 中 +`errno` 是线程局部变量,访问它要用一个 GOT 条目。没有 `.got` 输出段时,ld.lld 把这个条目放进 RAM, +`.bin` 从 Flash 一直延伸到 RAM(约 384 MiB),启动后条目中也不是链接时的值。因此 +`libxr stm32 cmake` 为没有 `.got` 输出段的链接脚本在 `.fini_array` 之后加入一段,放在与 +`.fini_array` 相同的存储区。ld.lld 要求 init/fini 数组和 GOT 连续,两者之间隔着其他输出段时, +STARM_NEWLIB 配置的链接报 `.got is not contiguous with other relro sections`;已有的 `.got` 不紧跟 +`.fini_array` 时只给出警告,不改动。CubeMX 重新生成代码时保留已有的链接脚本,GCC 构建不受这一段 +影响。 + +`libxr stm32 cmake` also handles the linker scripts (`*.ld`) in the root of the project. The +linker script CubeMX writes has no `.got` output section, while the `syscalls.c` and `sysmem.c` +CubeMX generates write `errno`; in the picolibc of ST Arm Clang `errno` is thread-local, and +reaching it takes a GOT entry. Without a `.got` output section ld.lld puts that entry in RAM, the +`.bin` spans Flash up to RAM (about 384 MiB), and after startup the entry does not hold the linked +value. `libxr stm32 cmake` therefore adds a `.got` output section after `.fini_array` to a linker +script without one, in the same memory region as `.fini_array`. ld.lld requires the init/fini +arrays and the GOT to be contiguous: with another output section between them, an STARM_NEWLIB +link fails with `.got is not contiguous with other relro sections`, and an existing `.got` that +does not directly follow `.fini_array` only gets a warning and stays as it is. CubeMX keeps an +existing linker script when it regenerates the code, and GCC builds are not affected by the +section. + --- ## 💾 Flash 布局 / Flash Layout diff --git a/src/libxr/generator_stm32_cmake.py b/src/libxr/generator_stm32_cmake.py index 75b83a8..b4226fa 100644 --- a/src/libxr/generator_stm32_cmake.py +++ b/src/libxr/generator_stm32_cmake.py @@ -2,11 +2,13 @@ """libxr stm32 cmake:把 LibXR 接入 CubeMX 生成的 CMake 工程。 libxr stm32 cmake: integrate LibXR into a CMake project generated by CubeMX. -整体生成 cmake/LibXR.CMake,把 ST Arm Clang 工具链文件恢复为 CubeMX 原样,并在 CMakeLists.txt -中 include LibXR.CMake。这些文件改变后,已有的构建目录在下次构建时由 CMake 重新配置。 +整体生成 cmake/LibXR.CMake,把 ST Arm Clang 工具链文件恢复为 CubeMX 原样,在链接脚本中把 .got +放进 Flash,并在 CMakeLists.txt 中 include LibXR.CMake。这些文件改变后,已有的构建目录在下次构建时 +由 CMake 重新配置。 It generates cmake/LibXR.CMake as a whole, restores the ST Arm Clang toolchain file to what -CubeMX wrote, and includes LibXR.CMake from CMakeLists.txt. CMake reconfigures existing build -directories on their next build after these files change. +CubeMX wrote, puts .got in Flash in the linker scripts, and includes LibXR.CMake from +CMakeLists.txt. CMake reconfigures existing build directories on their next build after these +files change. LibXR.CMake 的结构固定:“Project settings”块在最前,其后依次是编译选项的 User Code 区域、LibXR、 Application、库的优化设置和固件镜像(链接后由 ELF 生成 .hex 和 .bin),最后是应用设置的 User Code 区域。 @@ -36,6 +38,8 @@ from xr_syntax.cpp import CppDocument from xr_syntax.i18n import tr +from libxr.stm32_linker_script import place_got_in_project + # LibXR.CMake 第一行:说明文件由 libxr stm32 setup 生成,供用户修改的是设置块和两个 User Code 区域。 # The first line of LibXR.CMake: the file is generated by libxr stm32 setup, and the settings block # and the two User Code regions are the parts for the user to edit. @@ -1104,15 +1108,17 @@ def integrate(input_directory: str) -> None: """把 LibXR 接入 input_directory 中的 CubeMX CMake 工程。 Integrate LibXR into the CubeMX CMake project in input_directory. - 依次生成或重写 cmake/LibXR.CMake,把 cmake/starm-clang.cmake 恢复为 CubeMX 原样,并在 - CMakeLists.txt 没有 include LibXR.CMake 时(见 includes_libxr_cmake())追加一行。LIBXR_SYSTEM - 由 Core/Inc 中的 FreeRTOSConfig.h 或 app_threadx.h 决定,都没有时为 None。输入目录或 - CMakeLists.txt 不存在时以状态 1 退出。 + 依次生成或重写 cmake/LibXR.CMake,把 cmake/starm-clang.cmake 恢复为 CubeMX 原样,在根目录的 + 链接脚本中把 .got 放进 Flash、紧跟 .fini_array(见 stm32_linker_script),并在 CMakeLists.txt + 没有 include LibXR.CMake 时(见 includes_libxr_cmake())追加一行。LIBXR_SYSTEM 由 Core/Inc + 中的 FreeRTOSConfig.h 或 app_threadx.h 决定,都没有时为 None。输入目录或 CMakeLists.txt + 不存在时以状态 1 退出。 In order, it generates or rewrites cmake/LibXR.CMake, restores cmake/starm-clang.cmake to - what CubeMX wrote, and appends a line that includes LibXR.CMake to CMakeLists.txt when it - does not include it yet (see includes_libxr_cmake()). LIBXR_SYSTEM follows FreeRTOSConfig.h - or app_threadx.h in Core/Inc, and is None without either. A missing input directory or - CMakeLists.txt exits with status 1. + what CubeMX wrote, puts .got in Flash directly after .fini_array in the linker scripts of the + root (see stm32_linker_script), and appends a line that includes LibXR.CMake to + CMakeLists.txt when it does not include it yet (see includes_libxr_cmake()). LIBXR_SYSTEM + follows FreeRTOSConfig.h or app_threadx.h in Core/Inc, and is None without either. A missing + input directory or CMakeLists.txt exits with status 1. """ if not os.path.isdir(input_directory): @@ -1147,6 +1153,7 @@ def integrate(input_directory: str) -> None: update_or_create_libxr_cmake(file_path, system, project_uses_xrobot(input_directory)) restore_starm_clang_toolchain(os.path.join(cmake_dir, "starm-clang.cmake")) + place_got_in_project(input_directory) cmake_content = read_text_with_fallback(main_cmake_path) if not includes_libxr_cmake(cmake_content): diff --git a/src/libxr/stm32_linker_script.py b/src/libxr/stm32_linker_script.py new file mode 100644 index 0000000..49e71b9 --- /dev/null +++ b/src/libxr/stm32_linker_script.py @@ -0,0 +1,174 @@ +"""STM32 链接脚本中 GOT 的位置:.got 输出段放在 Flash 中,紧跟 .fini_array。 +The place of the GOT in an STM32 linker script: a .got output section in Flash, directly after +.fini_array. + +CubeMX 写出的链接脚本没有 .got 输出段,而 CubeMX 生成的 syscalls.c 和 sysmem.c 写 errno;ST Arm +Clang 的 picolibc 中 errno 是线程局部变量,访问它要用一个 GOT 条目。ld.lld 把这个孤立的段放进 RAM, +加载地址等于运行地址:.bin 从 Flash 一直覆盖到 RAM(约 384 MiB),启动代码复制 .data 时写进这个字的 +也不是链接时的值。静态镜像中的 GOT 条目是链接时就确定的常量,所以放在 Flash 中。ld.lld 把 init/fini +数组和 GOT 归入同一段 RELRO 范围,要求它们连续,所以 .got 紧跟 .fini_array;中间隔着别的输出段时, +STARM_NEWLIB 的链接报 ".got is not contiguous with other relro sections"。 +The linker script CubeMX writes has no .got output section, while the syscalls.c and sysmem.c +CubeMX generates write errno; in the picolibc of ST Arm Clang errno is thread-local, and reaching +it takes a GOT entry. ld.lld puts that orphan section in RAM with its load address equal to its +run address: the .bin spans Flash up to RAM (about 384 MiB), and the startup copy of .data does +not write the linked value into that word. GOT entries of a static image are constants fixed at +link time, so they go to Flash. ld.lld puts the init/fini arrays and the GOT in one RELRO range +that must be contiguous, so .got directly follows .fini_array; with another output section in +between, an STARM_NEWLIB link fails with ".got is not contiguous with other relro sections". +""" + +import logging +import re +from pathlib import Path + +from xr_syntax.i18n import tr + +# 插入的 .got 输出段,按 .fini_array 的缩进和内存区域写出。 +# The inserted .got output section, written with the indentation and memory region of .fini_array. +GOT_SECTION = """\ +{indent}/* GOT entries are link-time constants in this static image. ld.lld puts the init/fini +{indent} arrays and the GOT in one RELRO range, so .got must directly follow .fini_array. */ +{indent}.got : +{indent}{{ +{indent} . = ALIGN(4); +{indent} *(.got .got.*) +{indent} . = ALIGN(4); +{indent}}} {region} +""" + +# 一行开头的输出段定义:段名,可选的括号(如 (READONLY)、(NOLOAD)),其他属性,然后是冒号。 +# An output section definition at the start of a line: the name, an optional parenthesis such as +# (READONLY) or (NOLOAD), other attributes, then the colon. +_SECTION_HEADER = re.compile( + r"^(?P[ \t]*)(?P\.[\w.$]+|/DISCARD/)[ \t]*(?:\([^)\n]*\))?[^:;=\n{}]*:(?![=:])", + re.MULTILINE, +) +_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL) + + +def _without_comments(text: str) -> str: + """把 text 中的注释换成等长的空格(换行保留),使偏移量不变。 + Replace the comments of text with as many spaces (keeping the line breaks), so that offsets + stay the same. + """ + return _COMMENT.sub(lambda m: re.sub(r"[^\n]", " ", m.group()), text) + + +def _sections(code: str) -> list[re.Match]: + """code(已去掉注释)中 SECTIONS 块顶层的输出段定义,按出现顺序排列。 + The output section definitions at the top level of the SECTIONS block of code (with the + comments removed), in their order. + """ + found = [] + for match in _SECTION_HEADER.finditer(code): + start = match.start("name") + if code.count("{", 0, start) - code.count("}", 0, start) == 1: + found.append(match) + return found + + +def _closing_line_end(code: str, header: re.Match) -> tuple[int, str] | None: + """header 所定义的输出段结束的那一行:(该行末尾的偏移量, 右花括号之后的内存区域)。 + The line where the output section that header defines ends: (the offset of the end of that + line, the memory region after the closing brace). + + 找不到配对的花括号时为 None。 + None when the braces do not pair up. + """ + opening = code.find("{", header.end()) + if opening < 0: + return None + depth = 0 + for index in range(opening, len(code)): + if code[index] == "{": + depth += 1 + elif code[index] == "}": + depth -= 1 + if depth == 0: + line_end = code.find("\n", index) + line_end = len(code) if line_end < 0 else line_end + return line_end, code[index + 1 : line_end].strip() + return None + + +def place_got(file_path: str | Path) -> bool: + """确保链接脚本 file_path 中 .got 输出段紧跟 .fini_array;写回时返回 True。 + Make sure the .got output section of the linker script file_path directly follows + .fini_array; True when the file was written back. + + 没有 .got 输出段时,在 .fini_array 之后插入一个,内存区域与 .fini_array 相同。已有的 .got + 不紧跟 .fini_array 时只给出警告,用户写的段不动;没有 .fini_array 时也只警告。换行符和编码 + 保持文件原来的写法。 + Without a .got output section, one is inserted after .fini_array, in the memory region of + .fini_array. An existing .got that does not directly follow .fini_array only gets a warning, + and the user's section stays; a script without .fini_array also only gets a warning. The line + endings and the encoding stay as the file has them. + """ + path = Path(file_path) + raw = path.read_bytes() + eol = "\r\n" if b"\r\n" in raw else "\n" + text = raw.decode("utf-8", errors="surrogateescape").replace("\r\n", "\n") + code = _without_comments(text) + sections = _sections(code) + names = [match.group("name") for match in sections] + if ".fini_array" not in names: + logging.warning( + tr( + f"{path} has no .fini_array output section; the GOT placement is left unchanged", + f"{path} 中没有 .fini_array 输出段,GOT 的位置保持不变", + ) + ) + return False + fini = names.index(".fini_array") + if ".got" in names: + if fini + 1 < len(names) and names[fini + 1] == ".got": + return False + logging.warning( + tr( + f"{path}: the .got output section does not directly follow .fini_array, so an " + 'ST Arm Clang (ld.lld) link can fail with ".got is not contiguous with other ' + 'relro sections"; move it directly after .fini_array', + f"{path} 中的 .got 输出段没有紧跟 .fini_array,ST Arm Clang(ld.lld)链接可能报" + "“.got is not contiguous with other relro sections”;请把它移到 .fini_array 之后", + ) + ) + return False + closing = _closing_line_end(code, sections[fini]) + if closing is None: + logging.warning( + tr( + f"{path}: the braces of .fini_array do not pair up; the GOT placement is left " + "unchanged", + f"{path} 中 .fini_array 的花括号不配对,GOT 的位置保持不变", + ) + ) + return False + line_end, region = closing + block = GOT_SECTION.format(indent=sections[fini].group("indent"), region=region) + placed = text[:line_end] + "\n\n" + block.rstrip("\n") + text[line_end:] + path.write_bytes(placed.replace("\n", eol).encode("utf-8", errors="surrogateescape")) + logging.info( + tr( + f"Placed the GOT in {region.lstrip('>').strip()} after .fini_array in {path}", + f"已在 {path} 中把 GOT 放进 {region.lstrip('>').strip()},紧跟 .fini_array", + ) + ) + return True + + +def place_got_in_project(input_directory: str | Path) -> list[Path]: + """对 input_directory 根目录中的每个链接脚本(*.ld)调用 place_got(),返回写回的文件。 + Call place_got() on each linker script (*.ld) in the root of input_directory; return the + files that were written back. + + CubeMX 的 CMake 工程把链接脚本放在根目录;一个工程可以有多份(如 Flash 和 RAM 各一份)。 + A CubeMX CMake project keeps its linker scripts in the root; a project can have several + (one for Flash and one for RAM, for example). + """ + scripts = sorted( + entry + for entry in Path(input_directory).iterdir() + if entry.is_file() and entry.suffix.lower() == ".ld" + ) + return [script for script in scripts if place_got(script)] diff --git a/tests/test_stm32_linker_script.py b/tests/test_stm32_linker_script.py new file mode 100644 index 0000000..94bdb6c --- /dev/null +++ b/tests/test_stm32_linker_script.py @@ -0,0 +1,202 @@ +"""STM32 链接脚本中 GOT 的位置(libxr.stm32_linker_script):没有 .got 时插在 .fini_array 之后, +已有的 .got 不紧跟 .fini_array 时给出警告,重复运行不再改动。 +The place of the GOT in an STM32 linker script (libxr.stm32_linker_script): without .got one is +inserted after .fini_array, an existing .got that does not directly follow .fini_array gets a +warning, and running again changes nothing. +""" + +import tempfile +import textwrap +from pathlib import Path + +from fixtures import TestCase, logging_marker + +from libxr import generator_stm32_cmake as stm32_cmake +from libxr import stm32_linker_script as linker + +# CubeMX 6.18.1 为 STM32F407 写出的链接脚本中 .fini_array 前后的部分(省略号处有删节)。 +# The part around .fini_array of the linker script CubeMX 6.18.1 writes for an STM32F407 (cut +# where the ellipses are). +CUBEMX_SCRIPT = textwrap.dedent("""\ + /* Memories definition */ + MEMORY + { + CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K + RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K + FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K + } + + /* Sections */ + SECTIONS + { + /* ... */ + .init_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ + { + . = ALIGN(4); + PROVIDE_HIDDEN (__init_array_start = .); + KEEP (*(SORT(.init_array.*))) + KEEP (*(.init_array*)) + PROVIDE_HIDDEN (__init_array_end = .); + . = ALIGN(4); + } >FLASH + + .fini_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ + { + . = ALIGN(4); + PROVIDE_HIDDEN (__fini_array_start = .); + KEEP (*(SORT(.fini_array.*))) + KEEP (*(.fini_array*)) + PROVIDE_HIDDEN (__fini_array_end = .); + . = ALIGN(4); + } >FLASH + + _siccmram = LOADADDR(.ccmram); + + /* CCM-RAM section */ + .ccmram : + { + . = ALIGN(4); + _sccmram = .; /* create a global symbol at ccmram start */ + *(.ccmram) + *(.ccmram*) + . = ALIGN(4); + _eccmram = .; /* create a global symbol at ccmram end */ + } >CCMRAM AT> FLASH + + /* used by the startup to initialize data */ + _sidata = LOADADDR(.data); + + /* Initialized data sections goes into RAM, load LMA copy after code */ + .data : + { + . = ALIGN(4); + _sdata = .; /* create a global symbol at data start */ + *(.data) /* .data sections */ + *(.data*) /* .data* sections */ + . = ALIGN(4); + } >RAM AT> FLASH + + /* Initialized TLS data section */ + .tdata : ALIGN(4) + { + *(.tdata .tdata.* .gnu.linkonce.td.*) + . = ALIGN(4); + _edata = .; /* define a global symbol at data end */ + } >RAM AT> FLASH + /* ... */ + } + """) + +# 插入后的 .got 输出段。 +# The .got output section after the insertion. +GOT_BLOCK = """\ + + /* GOT entries are link-time constants in this static image. ld.lld puts the init/fini + arrays and the GOT in one RELRO range, so .got must directly follow .fini_array. */ + .got : + { + . = ALIGN(4); + *(.got .got.*) + . = ALIGN(4); + } >FLASH +""" +FINI_END = " PROVIDE_HIDDEN (__fini_array_end = .);\n . = ALIGN(4);\n } >FLASH\n" +PLACED_SCRIPT = CUBEMX_SCRIPT.replace(FINI_END, FINI_END + GOT_BLOCK) +# .got 在 CCM 段之后的脚本:ST Arm Clang 的 NEWLIB 配置链接失败的写法。 +# A script with .got after the CCM section: the layout an ST Arm Clang NEWLIB link fails on. +GOT_AFTER_CCM = CUBEMX_SCRIPT.replace( + " /* used by the startup", GOT_BLOCK.lstrip("\n") + "\n /* used by the startup" +) + + +class PlacedGot(TestCase): + """单个链接脚本中 .got 的插入、保留和警告。 + The insertion, keeping and warnings of .got in a single linker script. + """ + + def setUp(self): + super().setUp() + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.path = Path(temporary.name) / "STM32F407XX_FLASH.ld" + + def place(self, text): + """写入链接脚本并放置 GOT,返回 (是否写回, 结果)。 + Write the linker script, place the GOT and return (whether it was written, the result). + """ + self.path.write_bytes(text.encode("utf-8")) + changed = linker.place_got(self.path) + return changed, self.path.read_bytes().decode("utf-8") + + def test_a_cubemx_script_gets_got_directly_after_fini_array(self): + self.assertEqual(self.place(CUBEMX_SCRIPT), (True, PLACED_SCRIPT)) + + def test_running_again_changes_nothing(self): + self.place(CUBEMX_SCRIPT) + with self.assertLogs(level="INFO") as logs: + logging_marker() + self.assertEqual(self.place(PLACED_SCRIPT), (False, PLACED_SCRIPT)) + self.assertEqual(logs.output, ["WARNING:root:marker"]) + + def test_line_endings_stay_as_the_file_has_them(self): + crlf = CUBEMX_SCRIPT.replace("\n", "\r\n") + self.assertEqual(self.place(crlf), (True, PLACED_SCRIPT.replace("\n", "\r\n"))) + + def test_the_got_takes_the_memory_region_of_fini_array(self): + rom = CUBEMX_SCRIPT.replace("FLASH", "ROM") + self.assertEqual(self.place(rom), (True, PLACED_SCRIPT.replace("FLASH", "ROM"))) + + def test_got_in_comments_or_input_rules_does_not_count(self): + # 更早的 BSP 在 .data 中加载 GOT;插入的 .got 在前,先取走这些输入段。 + # Earlier BSPs loaded the GOT with .data; the inserted .got comes first and takes those + # input sections. + with_rule = CUBEMX_SCRIPT.replace( + " *(.data*) /* .data* sections */\n", + " *(.data*) /* .data* sections */\n" + " *(.got .got.*) /* .got : loaded with .data */\n", + ) + changed, text = self.place(with_rule) + self.assertTrue(changed) + self.assertEqual(text, with_rule.replace(FINI_END, FINI_END + GOT_BLOCK)) + + def test_a_got_elsewhere_is_left_alone_with_a_warning(self): + with self.assertLogs(level="WARNING") as logs: + self.assertEqual(self.place(GOT_AFTER_CCM), (False, GOT_AFTER_CCM)) + self.assertEqual(len(logs.output), 1) + self.assertIn("does not directly follow .fini_array", logs.output[0]) + + def test_a_script_without_fini_array_is_left_alone_with_a_warning(self): + script = "SECTIONS\n{\n .text :\n {\n *(.text*)\n } >FLASH\n}\n" + with self.assertLogs(level="WARNING") as logs: + self.assertEqual(self.place(script), (False, script)) + self.assertIn("has no .fini_array output section", logs.output[0]) + + +class ProjectScripts(TestCase): + """工程根目录中的每份链接脚本,以及 libxr stm32 cmake 中的这一步。 + Every linker script in the root of a project, and this step of libxr stm32 cmake. + """ + + def setUp(self): + super().setUp() + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + + def test_every_linker_script_in_the_root_is_placed(self): + (self.root / "STM32F407XX_FLASH.ld").write_text(CUBEMX_SCRIPT, encoding="utf-8") + (self.root / "STM32F407XX_RAM.ld").write_text(PLACED_SCRIPT, encoding="utf-8") + (self.root / "notes.txt").write_text(CUBEMX_SCRIPT, encoding="utf-8") + written = linker.place_got_in_project(self.root) + self.assertEqual(written, [self.root / "STM32F407XX_FLASH.ld"]) + for name in ("STM32F407XX_FLASH.ld", "STM32F407XX_RAM.ld"): + self.assertEqual((self.root / name).read_text(encoding="utf-8"), PLACED_SCRIPT) + self.assertEqual((self.root / "notes.txt").read_text(encoding="utf-8"), CUBEMX_SCRIPT) + + def test_integrating_cmake_places_the_got(self): + (self.root / "CMakeLists.txt").write_text("project(demo)\n", encoding="utf-8") + (self.root / "STM32F407XX_FLASH.ld").write_text(CUBEMX_SCRIPT, encoding="utf-8") + stm32_cmake.integrate(str(self.root)) + self.assertEqual( + (self.root / "STM32F407XX_FLASH.ld").read_text(encoding="utf-8"), PLACED_SCRIPT + )