强制将特定的编译器生成变量放入特定的ELF节(使用gcc)

12

我先从最核心的问题开始:在使用gcc的C语言中,是否可能获取__func__(或等价的__FUNCTION__)的值,并将其存储在除.rodata(或-mrodata=所指示的位置)或其子节之外的某个段中?

完整解释如下:

假设我有一个日志宏:

#define LOG(fmt, ...) log_internal(__FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__)

(如果使用单目上下文中的字符串连接运算符##,仅当__VA_ARGS__列表为空时,才会消耗前面的逗号,从而允许在格式化字符串中使用带有或不带有参数的用法。)

然后我可以正常使用这个宏:

void my_function(void) {
    LOG("foo!");
    LOG("bar: %p", &bar);
}

可能会打印以下内容(显然取决于log_internal的实现):

foo.c:201(my_function) foo!
foo.c:202(my_function) bar: 0x12345678
在这种情况下,格式字符串("foo""bar:%p")以及预处理器字符串("foo.c""my_function")是匿名只读数据,并且它们会自动放置到.rodata节中。
但是如果我想将它们放到另一个地方(我正在运行几乎所有东西都从RAM中运行的嵌入式平台,以获得速度,但内存限制正在推动某些东西进入ROM),移动__FILE__和格式字符串就很“容易”。
#define ROM_STR(str) (__extension__({static const __attribute__((__section__(".rom_data"))) char __c[] = (str); (const char *)&__c;}))
#define LOG(fmt, ...) log_internal(ROM_STR(__FILE__), __LINE__, __func__, ROM_STR(fmt), ##__VA_ARGS__)
你无法在匿名字符串上使用__attribute__,所以ROM_STR宏会给它一个临时名称,将其附加到特定的部分,然后计算出起始地址,以便可以干净地替换。如果您尝试将char *变量作为格式字符串传递给LOG,则此方法不起作用,但我愿意排除该用例。
通常,编译器将恰好相同的匿名字符串组合成单个存储位置,因此一个文件中的每个__FILE__实例都将共享相同的运行时地址。通过在ROM_STR中显式命名,每个实例都将获得自己的存储位置,因此在__FILE__上使用它可能实际上并没有意义。
然而,我想在__func__上使用它。问题是__func____FILE__不是同一种魔术。根据gcc手册中的“函数名称作为字符串”:
  

编译器隐式声明了标识符__func__,就好像在每个函数定义的左括号后立即声明:

static const char __func__[] = "function-name";

在函数内部出现一个标识符function-name,其中function-name是词法上封闭函数的名称。这个名称是函数的未装饰名称。 ... 这些标识符不是预处理器宏。在GCC 3.3及更早版本以及仅限于C语言中,__FUNCTION____PRETTY_FUNCTION__被视为字符串字面量;它们可以用来初始化char数组,并且可以与其他字符串字面量连接。GCC 3.4及更高版本将它们视为变量,就像__func__一样。

因此,如果您将__func__ROM_STR包装在一起,您将得到:

error: invalid initializer

如果你试图在使用__func__之前或之后添加 section 属性,你会得到:

error: expected expression before ‘__attribute__’
或者
error: expected ‘)’ before ‘__attribute__’

那么我们又回到了最初的问题:是否有可能将__func__存储在我选择的某个部分中?也许我可以使用-fdata-sections以及一些链接脚本魔法来排除.rodata之外的.rodata.__func__.*?如果可以,那么链接脚本中用于排除的glob语法是什么?换句话说,在某个地方你会有一个*(.rodata*) - 我可以将*(.rodata.__func__*)放在其他地方,但我需要修改原始的glob以将其排除,这样我就不会得到两份副本。

2个回答

3

一种可能比你想象中更加hacky的方法是插入一个脚本,在编译和汇编之间更改部分名称。例如:

gcc -fdata-sections -S -o test.s test.c
sed 's/^\t.section\t\.rodata\.__func__\.[0-9]*/\t.section .rom_data/' -i test.s
gcc -c test.s

你可以尝试编写一个clang转换通道,将__func__声明放置在您选择的部分,或者编写一个使用libbfd的对象文件操作程序。

我认为在中间汇编文件中重命名部分的两步过程肯定可行。通过额外的(非平凡的)智能,脚本甚至可以在没有使用“-fdata-sections”选项构建时挑选出适当的数据。但是我的实验和进一步阅读链接器脚本表明,将这些部分显式地放置要容易得多,因此我在另一个答案中详细说明了这一点。 - Eric Angell
还有,BFD链接加一分。这对许多事情都可能会很方便。 - Eric Angell

2
看起来我在最后用-fdata-sections解决了问题,只是我没有足够理解GNU链接器。只要我首先指定*(.rodata.__func__*),我就不需要排除任何部分。与该模式匹配的任何部分都将被标记为已使用,因此稍后对*(.rodata*)的匹配不会重复计算并将其复制到其他位置。我根本不需要使用ROM_STR标记它们。很酷!
需要注意的是,-fdata-sections确实将每个函数字符串放入其自己的.rodata.__func__.1234节中(我不确定数字遵循什么模式)。我不知道匿名字符串是否也有自己的节;如果是这样,我可以使用相同的链接器技巧捕获所有匿名字符串,而不是ROM_STR节属性宏,但这可能是一个坏主意。ROM_STRLOG宏中使用,因此保证仅应用于日志格式字符串。如果我强制将所有匿名字符串转换为ROM,则会包括正常消息数据,并且我将支付运行时性能惩罚以从闪存中访问它。所以我不知道是否可能,但其可行性取决于您的具体系统要求。

这里有一个方便的链接脚本语法参考 - Eric Angell
1
截至2015年,gcc不再这样做。请参见:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=192 - David Given

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接