背景
之前的 blog
66、【OS】【Nuttx】【构建】:map文件生成(上)
67、【OS】【Nuttx】【构建】:map文件生成(中)
已经分析了 System.map 文件的生成逻辑,下面继续来分析
System.map 文件
之前的 blog 分析过,System.map 文件是从 nm 工具输出中提取的,里面大致包含了如下类型,下面来分析其中内容
System.map 文件这里定义了符号,符号类型,以及符号所在地址三种基本信息,下面来看下常见的一些符号类型
- T:text 段(代码段),表示全局函数
- t:text 段(代码段),表示静态局部函数
- D:data 段(已初始化数据段),表示已初始化的全局变量
- d:data 段(已初始化数据段),表示已初始化的静态局部变量
- B:bss 段(未初始化数据段),表示未初始化的全局变量
- b:bss 段(未初始化数据段),表示未初始化的静态局部变量
- R:rodata 段(只读数据段),表示全局常量
- r:rodata 段(只读数据段),表示静态局部常量
- W:全局弱符号定义
- U:未定义符号,需要链接时解析
这里特别看下 R/r 符号类型,比如这里放到 rodata 段的一些变量
可以看到都是字符串变量
再查看 g_tty_ops 这样的静态局部变量,发现它被分配到了 data 段(类型为 d),而不是预期的 rodata 段(只读段,类型为 r ),这是为什么呢?
带指针的全局变量
查看该变量定义
/****************************************************************************
* Private Function Prototypes
****************************************************************************/
static int tty_setup(struct uart_dev_s *dev);
static void tty_shutdown(struct uart_dev_s *dev);
static int tty_attach(struct uart_dev_s *dev);
static void tty_detach(struct uart_dev_s *dev);
static int tty_ioctl(struct file *filep, int cmd,
unsigned long arg);
static int tty_receive(struct uart_dev_s *dev, uint32_t *status);
static void tty_rxint(struct uart_dev_s *dev, bool enable);
static bool tty_rxavailable(struct uart_dev_s *dev);
static bool tty_rxflowcontrol(struct uart_dev_s *dev,
unsigned int nbuffered, bool upper);
#ifdef CONFIG_SIM_UART_DMA
static void tty_dmatxavail(struct uart_dev_s *dev);
static void tty_dmasend(struct uart_dev_s *dev);
static void tty_dmarxfree(struct uart_dev_s *dev);
static void tty_dmareceive(struct uart_dev_s *dev);
#endif
static void tty_send(struct uart_dev_s *dev, int ch);
static void tty_txint(struct uart_dev_s *dev, bool enable);
static bool tty_txready(struct uart_dev_s *dev);
static bool tty_txempty(struct uart_dev_s *dev);
/****************************************************************************
* Private Data
****************************************************************************/
static const struct uart_ops_s g_tty_ops __attribute__((section(".rodata"))) =
{
.setup = tty_setup,
.shutdown = tty_shutdown,
.attach = tty_attach,
.detach = tty_detach,
.ioctl = tty_ioctl,
.receive = tty_receive,
.rxint = tty_rxint,
.rxavailable = tty_rxavailable,
.rxflowcontrol = tty_rxflowcontrol,
#ifdef CONFIG_SIM_UART_DMA
.dmatxavail = tty_dmatxavail,
.dmasend = tty_dmasend,
.dmarxfree = tty_dmarxfree,
.dmareceive = tty_dmareceive,
#endif
.send = tty_send,
.txint = tty_txint,
.txready = tty_txready,
.txempty = tty_txempty,
};
可以看到,虽然该结构体变量被声明为 const,虽然 tty_setup… 等是一个局部静态函数(作用域只在当前编译单元),但在大多数现代工具链(如 gcc、clang)中,只要结构体中包含函数指针,编译器就默认将其放在 data 段,而不是 rodata 段,这是出于以下考虑:
- 函数指针可能需要在链接阶段甚至运行时(动态库里的函数)被重定位修正(relocation)
- 即使是本地静态函数,某些架构(如 arm、risc-v)也可能要求对函数地址进行间接跳转处理,此时函数地址也不是静态确定的,不能直接放入只读段
- rodata 段通常用于存储纯常量数据,如字符串字面量,数值常量
总的说来,尽管这些结构体变量是 const 的,但编译器无法保证会不会通过某种方式绕过类型系统去修改这些数据,为了安全起见,编译器会保守地将含有指针的 const 结构体放在 data 段处理
未定义符号
下面再来看下 U 类型(未定义符号)
上述符号表示他们是在特定版本的 GNU C Library (glibc) 中定义的函数,这里 func@GLIBC_2.2.5 要求运行时系统上的 glibc 版本至少为 2.2.5 才能正常运行
那么为什么会出现这些未引用的符号?
- 当编译一个需要动态链接程序的时候,并不会把所有库函数打包进可执行文件。相反,它只是记录下该程序需要哪些函数和对应的 glibc 版本
- 此时,编译器会生成一个 elf 文件,其中包含对 glibc 函数的引用,而不是将这些函数的代码复制进去,从而可以减少可执行程序体积