在实现完整的链接器之前,让我们先从一个简单的工具开始热身——实现nm命令。这个工具在Unix系统中广泛使用,它的作用是显示目标文件或可执行文件中的符号表。通过实现这个工具,你将熟悉如何遍历和解释符号信息,这是后续链接器工作的基础。
你有没有遇到过这样的错误?
undefined reference to 'printf'
multiple definition of 'main'
这些错误都与符号有关。符号就像程序的"索引"——每个函数、每个全局变量都有一个名字,这个名字就是符号。链接器的核心工作之一就是确保每个被引用的符号都能找到它的定义。
让我们通过一个例子来理解符号的不同类型:
// example.c
static int counter = 0; // 文件内可见
int shared = 42; // 其他文件可见
extern void print(int x); // 在其他文件定义
void count() { // 其他文件可见
counter++;
print(counter);
}这段代码中包含了四个符号,但它们的"可见性"各不相同。counter是静态符号,只在example.c内部可见,其他文件无法引用它。shared和count是全局符号,可以被其他文件引用。print是未定义符号,表示它在当前文件中被使用,但定义在其他地方。
当你用编译器处理这个文件时,生成的目标文件会包含符号表,记录这些符号的信息。你的任务是实现一个工具来读取和显示这个符号表。
传统的nm工具输出三列信息,每行描述一个符号。对于上面的例子,输出可能是这样的:
0000000000000000 d counter
0000000000000000 D shared
0000000000000000 T count
第一列是符号的地址,用十六进制表示,占16个字符位宽,左侧补零。
Important
在目标文件中,符号的"地址"其实是它在所在节中的偏移量,而不是最终在内存中的绝对地址。比如counter在.data节的偏移0处,count在.text节的偏移0处。它们看起来地址相同,但实际上位于不同的节中,不会冲突。只有链接器确定了每个节在内存中的位置后,才能计算出符号的最终绝对地址。
第二列是符号的类型码,这是一个单字符,编码了符号的两个属性:它在哪个节中,以及它是全局的还是局部的。类型码的规则是这样的:
大写字母表示全局符号(可以被其他文件引用),小写字母表示局部符号(只在当前文件内可见)。字母本身表示符号所在的节:T或t表示代码段(.text),D或d表示数据段(.data),B或b表示BSS段(.bss),R或r表示只读数据段(.rodata)。
还有两个特殊的类型码:W表示弱符号在代码段,V表示弱符号在数据段或BSS段(你会在任务四中详细学习弱符号)。
Note
未定义的符号用U表示,但在这个任务中,我们不显示未定义符号——因为它们还没有地址,显示它们的意义不大。
第三列是符号的名字,直接从符号表中获取。
现在让我们思考如何实现这个工具。你需要补充的函数签名是:
void FLE_nm(const FLEObject& obj);这个函数接收一个已经解析好的FLEObject。你的任务是遍历符号表,为每个符号生成一行输出。
第一步是遍历符号。FLEObject::symbols是一个std::vector<Symbol>,你可以用范围for循环或迭代器来遍历它。对于每个符号,你需要判断它是否应该被显示。只有已定义的符号才显示——检查symbol.section字段是否为空字符串即可。
第二步是确定符号的类型码。这需要检查符号所在的节(通过symbol.section)以及符号的类型(通过symbol.type)。
第三步是格式化输出。注意地址需要输出为16位十六进制数,左侧补零。
Tip
格式化输出可以使用:
std::cout << std::setw(16) << std::setfill('0') << std::hex << addr; // C++ 风格或
printf("%016lx", addr); // C 风格,输出16位的十六进制数,左侧补0以及,在 C++ 20 后,std::format 和 std::print 提供了更简洁的格式化方案:
std::print("{:016x}", addr); // Modern C++ 风格节名除了标准的.text、.data等,还可能有带后缀的变体,比如.text.startup、.rodata.str1.1。这些变体应该归入它们的基本类别——以.text开头的都是代码段,以.rodata开头的都是只读数据段。你可以用std::string::starts_with(C++20)或std::string::compare来检查前缀。
另一个需要注意的是弱符号的判断。弱符号可能定义在任何节中,你需要同时检查符号类型和所在的节。如果符号类型是WEAK且在代码段,使用W;如果在数据段或BSS段,使用V。
最后,输出的顺序在这个任务中并不重要。真实的nm工具通常会按名字排序,但我们的测试不要求这个。你可以按照符号在符号表中出现的顺序输出,不需要排序。
现在请在src/student/nm.cpp中实现FLE_nm函数。建议从最简单的情况开始——先处理代码段的全局符号,确保格式正确后,再扩展到其他类型。
完成后,运行测试来验证:
make test_1