在嵌入式开发和系统编程中,有时候我们需要对程序的布局进行精细控制,以便更好地管理内存资源。Linux
下的链接器(ld
)提供了强大的功能,可以让我们在链接阶段定制程序的布局。本文将介绍如何利用 Linker Script
结合 C
语言中的 __attribute__((section))
属性来实现函数的自定义排列,并通过一个简单的示例展示如何遍历这些函数并调用它们。
在编写大型应用程序或嵌入式系统软件时,对程序的内存布局进行优化是非常重要的。通过使用 Linker Script
,我们可以精确地控制程序的各个部分如何在内存中布局。此外,C
语言的__attribute__((section))
属性允许我们将特定的数据或函数放置到指定的section
中。这种技术在实现初始化函数列表、配置选项等方面非常有用。
为了演示这一技术的应用,我们编写了一个简单的 C
程序,该程序定义了一个结构体来存储命令及其帮助信息,并将这些命令放置在一个自定义的 section
中。我们还将基于默认的Linker Script
来定义这个 section
,并在 main
函数中遍历这些命令并调用它们。同时,利用 #
和 ##
定义命令宏,再增加命令时,只需实现命令的功能函数后, 调用 ADD_CMD
宏即可。
源代码 (test.c
)如下:
#include
typedef struct{
const char *name; /* 命令的名字 */
const char *help; /* 帮助信息 */
void (*func)(void); /*命令的功能函数指针*/
} cmd_t;
#define __my_section __attribute__ ((section(".mysection"), aligned(8)))
#define ADD_CMD(name, help, func) \
cmd_t __cmd_##name __my_section = {#name, help, func}
void do_cmd1(void)
{
printf("This is %s function.\n", __func__);
}
ADD_CMD(cmd1, "help of cmd1", do_cmd1);
void do_cmd2(void)
{
printf("This is %s function.\n", __func__);
}
ADD_CMD(cmd2, "help of cmd2", do_cmd2);
extern cmd_t __mysection_start;
extern cmd_t __mysection_stop;
int main(void)
{
cmd_t *p = &__mysection_start;
for(; p < &__mysection_stop; p++)
{
printf("========================\n");
printf("%s\n", p->name);
p->func();
printf("========================\n");
}
return 0;
}
使用 ld -verbose > test.lds
把编译器默认的链接脚本输出到 test.lds
文件,修改链接脚本 (test.lds
),增加 __mysection_start
和 __mysection_end
用于记录特定 section
的开始地址与结束地址,*(.mysection)
表示所有的 .o
文件的 .mysection
节内容集中放入输出文件的 .mysection
节,如下:
/* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2018 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("elf64-x86-64", "elf64-x86-64",
"elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/x86_64-redhat-linux/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/x86_64-redhat-linux/lib"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib");
SECTIONS
{
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
__mysection_start = . ;
. = ALIGN(8);
.mysection : { *(.mysection) }
__mysection_stop = . ;
.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) }
.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.*)
*(.rela.ldata .rela.ldata.* .rela.gnu.linkonce.l.*)
*(.rela.lbss .rela.lbss.* .rela.gnu.linkonce.lb.*)
*(.rela.lrodata .rela.lrodata.* .rela.gnu.linkonce.lr.*)
*(.rela.ifunc)
}
.rela.plt :
{
*(.rela.plt)
PROVIDE_HIDDEN (__rela_iplt_start = .);
*(.rela.iplt)
PROVIDE_HIDDEN (__rela_iplt_end = .);
}
.init :
{
KEEP (*(SORT_NONE(.init)))
}
.plt : { *(.plt) *(.iplt) }
.plt.got : { *(.plt.got) }
.plt.sec : { *(.plt.sec) }
.text :
{
*(.text.unlikely .text.*_unlikely .text.unlikely.*)
*(.text.exit .text.exit.*)
*(.text.startup .text.startup.*)
*(.text.hot .text.hot.*)
*(.text .stub .text.* .gnu.linkonce.t.*)
/* .gnu.warning sections are handled specially by elf32.em. */
*(.gnu.warning)
}
.fini :
{
KEEP (*(SORT_NONE(.fini)))
}
PROVIDE (__etext = .);
PROVIDE (_etext = .);
PROVIDE (etext = .);
.rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
.rodata1 : { *(.rodata1) }
.eh_frame_hdr : { *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) }
.eh_frame : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) }
.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
.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. */
. = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
/* Exception handling */
.eh_frame : ONLY_IF_RW { KEEP (*(.eh_frame)) *(.eh_frame.*) }
.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 .exception_ranges*) }
/* Thread Local Storage sections */
.tdata : { *(.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) *(.igot) }
. = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 24 ? 24 : 0, .);
.got.plt : { *(.got.plt) *(.igot.plt) }
.data :
{
*(.data .data.* .gnu.linkonce.d.*)
SORT(CONSTRUCTORS)
}
.data1 : { *(.data1) }
_edata = .; PROVIDE (edata = .);
. = .;
__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 don't
pad the .data section. */
. = ALIGN(. != 0 ? 64 / 8 : 1);
}
.lbss :
{
*(.dynlbss)
*(.lbss .lbss.* .gnu.linkonce.lb.*)
*(LARGE_COMMON)
}
. = ALIGN(64 / 8);
. = SEGMENT_START("ldata-segment", .);
.lrodata ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) :
{
*(.lrodata .lrodata.* .gnu.linkonce.lr.*)
}
.ldata ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) :
{
*(.ldata .ldata.* .gnu.linkonce.l.*)
. = ALIGN(. != 0 ? 64 / 8 : 1);
}
. = ALIGN(64 / 8);
_end = .; PROVIDE (end = .);
. = DATA_SEGMENT_END (.);
/* 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 : { *(.comment) }
.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 Extension. */
.debug_macro 0 : { *(.debug_macro) }
.debug_addr 0 : { *(.debug_addr) }
.gnu.attributes 0 : { KEEP (*(.gnu.attributes)) }
/DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) *(.gnu_object_only) }
}
我们需要编译上述源代码和链接脚本。我们可以使用以下命令进行编译:
gcc test.c -T test.lds -o test -g -Wl,-Map=test.map
【拓展:映射文件】在使用
gcc
编译器时,-Wl,-Map=test.map
选项用于生成一个映射文件(map file
),这个文件通常命名为test.map
。映射文件提供了关于最终链接生成的可执行文件或库的详细信息,包括但不限于:
Section 布局:映射文件显示了每个 section
(如.text
,.data
,.bss
,.rodata
等)的起始地址、大小和位置。符号表:映射文件列出了所有的全局符号(如函数和变量),包括它们的地址和大小。 输入文件:映射文件记录了所有参与链接过程的输入文件(如目标文件和库文件)的信息。 未解析的符号:如果存在任何未解析的符号(即找不到定义的符号),映射文件也会指出这些问题。 生成映射文件的作用
调试和验证:
映射文件可以帮助开发者验证链接过程是否按预期进行。 它可以用于检查是否有未解析的符号或其他链接错误。 对于复杂项目,映射文件可以帮助确认各个模块是否被正确链接。 内存布局分析:
开发者可以通过映射文件了解程序的内存布局,这对于嵌入式系统和资源受限环境尤为重要。 映射文件可以揭示哪些 section 占用了多少内存,帮助优化内存使用。 定位问题:
当程序出现奇怪的行为时,映射文件可以提供有关内存布局和符号位置的重要线索,帮助定位问题。 特别是在多文件或多模块项目中,映射文件可以帮助识别哪些符号来自哪个文件。
编译完成后,我们会得到一个名为 test
的可执行文件以及一个名为 test.map
的地图文件。运行这个程序:
[root@localhost custom_section]# ./test
========================
cmd1
This is do_cmd1 function.
========================
========================
cmd2
This is do_cmd2 function.
========================
通过运行结果显示,主函数通过链接脚本定义的两个全局变量 __mysection_start
和__mysection_stop
以及 cmd_t
类型的指针 p
遍历了 .mysection
中的所有命令,并依次调用它们的名字和功能函数。
结构体定义:
typedef struct {
const char *name; /* 命令的名字 */
const char *help; /* 帮助信息 */
void (*func)(void); /* 命令的功能函数指针 */
} cmd_t;
我们定义了一个 cmd_t
结构体,其中包含命令的名称、帮助信息和一个指向功能函数的指针。
宏定义:
#define __my_section __attribute__((section(".mysection"), aligned(8)))
使用 __attribute__((section(".mysection"), aligned(8)))
将结构体实例放置到名为.mysection
的 section
中,并对其进行了 8
字节对齐。
注意:具体对齐方式可能因系统的不同而有所不同,请结合实际系统进行设置,一旦对齐方式设置错误,会引起程序
core dump
。
命令添加宏:
#define ADD_CMD(name, help, func) \
cmd_t __cmd_##name __my_section = {#name, help, func}
该宏允许我们在程序中方便地添加命令,每个命令都会被放置到 .mysection
中。#
操作符用于将宏参数转换成字符串。当宏被展开时,#
后面的宏参数会被转换成一个字符串字面量。这意味着宏参数会被原样保留,而不是先进行宏替换后再转换成字符串。##
操作符用于连接(拼接)两个标识符或者字符串。当宏被展开时,##
前后的两个操作数会被拼接成一个单一的操作数。
因此,文中的宏展开过程如下:
ADD_CMD(cmd1, "help of cmd1", do_cmd1);
宏展开后变为:
cmd_t __cmd_cmd1 __attribute__ ((section(".mysection"), aligned(8))) = {"cmd1", "help of cmd1", do_cmd1};
功能函数:
void do_cmdx(void)
{
printf("This is %s function.\n", __func__);
}
定义了 2
个简单的功能函数 do_cmdx
。
外部变量声明:
extern cmd_t __mysection_start;
extern cmd_t __mysection_stop;
声明了两个全局变量,用于标识.mysection
的起始和结束位置。可能有人会疑惑,链接脚本中定义的这两个变量明明没有类型,为什么这里用 extern
声明的时候却变成了 cmd_t
类型?在链接器脚本中,__mysection_start
和 __mysection_stop
用来标记 .mysection
的起始和结束位置,它们不需要类型信息。在 C
代码中,__mysection_start
和 __mysection_stop
必须声明为 cmd_t
类型,这样才能在 C
代码中正确地访问这些变量,即我们想要访问什么类型的数据,则必需要将这两个符号声明为对应的类型。通过这种方式,我们可以确保在 C
代码中正确地使用这些符号,并在链接器脚本中正确地布局程序的各个部分。
主函数:
int main(void)
{
cmd_t *p = &__mysection_start;
for (; p < &__mysection_stop; p++)
{
printf("========================\n");
printf("%s\n", p->name);
p->func();
printf("========================\n");
}
return 0;
}
主函数遍历 .mysection
中的所有命令,并依次调用它们的功能函数。你使用 p
指针来遍历 .mysection
节中的所有 cmd_t
结构体。p
指针从 __mysection_start
的地址开始,一直增加到 __mysection_stop
的地址之前。每次循环,p
都会指向下一个 cmd_t
结构体,直到达到 .mysection
节的末尾。
【拓展】
cmd_t *p = &__mysection_start
,这里为什么要用&__mysection_start
,而不是__mysection_start
,__mysection_start
本身不就存储的地址值吗?解析:
__mysection_start
和__mysection_stop
被定义为cmd_t
类型的全局变量。因此,如果直接使用__mysection_start
或__mysection_stop
,它们会是cmd_t
类型的值,而不是指针。cmd_t *p = &__mysection_start;
表示p
是一个指向cmd_t
类型的指针,而&__mysection_start
是cmd_t
类型变量__mysection_start
的地址。如果不使用&
,则编译器会报错,因为它期望一个指针类型的值,而您却提供了一个cmd_t
类型的值。为了确保指针变量p
正确初始化,并且在比较时使用正确的类型,您应该始终使用取地址运算符&
。
全局变量定义:
__mysection_start = .;
. = ALIGN(8);
.mysection : { *(.mysection) }
__mysection_stop = .;
链接脚本定义了两个全局变量 __mysection_start
和 __mysection_stop
,它们分别指向 .mysection
的起始和结束位置。通过 . = ALIGN(8)
;保证了对齐要求。
通过上述示例,我们展示了如何使用 Linker Script
和 C
语言的__attribute__((section))
属性来实现对程序布局的定制。这种方法不仅有助于优化内存布局,还可以在初始化函数列表、配置选项等方面发挥重要作用。希望这篇实战文章能帮助读者理解和应用这一技术。