【问题标题】:embed string via header that cannot be optimized away通过无法优化的标头嵌入字符串
【发布时间】:2019-04-11 14:23:17
【问题描述】:

在开发仅包含标头的库时,我想确保给定的字符串嵌入到使用我的标头的所有二进制文件中,即使编译器被配置为优化掉未使用的常量,并且二进制文件被剥离。

嵌入不应有任何副作用(除了使生成的二进制文件更大一点)。

我不知道人们会如何使用标题,但是

  • 头文件可能包含在多个编译单元中,所有编译单元都链接到一个二进制文件中
  • 目标平台是 Linux/macOS/Windows
  • 编译器很可能是 gcc/clang/MSVC

我的微不足道的尝试相当于:

static char frobnozzel_version_string[] = "Frobnozzel v0.1; © 2019 ACME; GPLv3";

...,但是在构建过程中很容易删除它(因为字符串实际上并没有被使用,它很容易成为优化编译器的牺牲品)。

所以问题是:是否可以在包含给定标头的任何二进制文件中嵌入一个字符串,该字符串不会被构建“发布”二进制文件的常规策略优化/剥离?

我知道,使用该库的任何人都可以(手动)删除我放入的任何内容,但我们假设,人们只是“按原样”使用标题。


上下文:有问题的标头在GPL 下发布,我希望能够检查用户是否真的遵守了许可。

【问题讨论】:

  • 宏可用于此。
  • 使用volatile限定符en.cppreference.com/w/cpp/language/cv
  • @sturcotte06 怎么样?
  • 通常,项目有一个version.h.in,它是从构建系统配置的,带有一堆定义,例如#define FROBNOZZEL_VERSION "@PROJECT_VERSION@"#define FROBNOZZEL_LICENSE "@PROJECT_LICENSE@"
  • @umläute:“我不认为这是一个大问题”如果整个项目的源代码适合一个屏幕,那么它可能不是大问题。如果 3 个人以上在一个相当大的代码库上工作,这将成为一个大问题。

标签: c compiler-optimization copyright-display


【解决方案1】:

您可以在标题中嵌入程序集伪操作,它应该保留(尽管从未使用过):

asm(".ascii \"Frobnozzel v0.1; © 2019 ACME; GPLv3\"\n\t");

请注意,这是 GCC/Clang 特定的。

MSVC 的替代方案是使用#pragma comment__asm db

__asm db "Frobnozzel v0.1; © 2019 ACME; GPLv3"
#pragma comment(user, "Frobnozzel v0.1; © 2019 ACME; GPLv3")

这是一个例子:

chronos@localhost ~/Downloads $ cat file.c 
#include <stdio.h>

#include "file.h"

int main(void)
{
        puts("The string is never used.");
}
chronos@localhost ~/Downloads $ cat file.h
#ifndef FILE_H
#define FILE_H 1

#if defined(__GNUC__)
    asm(".ascii \"Frobnozzel v0.1; © 2019 ACME; GPLv3\"\n\t");
#elif defined(_MSC_VER)
# if defined(_WIN32)
    __asm db "Frobnozzel v0.1; © 2019 ACME; GPLv3"
# elif defined(_WIN64)
#  pragma comment(user, "Frobnozzel v0.1; © 2019 ACME; GPLv3")
# endif
#endif
chronos@localhost ~/Downloads $ gcc file.c
chronos@localhost ~/Downloads $ grep "Frobnozzel v0.1; © 2019 ACME; GPLv3" a.out
Binary file a.out matches
chronos@localhost ~/Downloads $ 

gcc命令换成clang,结果是一样的。

对于 64 位 Windows,这需要将 user 替换为已弃用的 exestr 或创建将字符串嵌入可执行文件的资源文件。这样,链接时字符串将被删除。

【讨论】:

  • 请注意,MSVC 解决方案不适用于 x64,其中 __asm 被禁止。他们需要通过链接器指令来做到这一点。
  • @Mgetz x64 是什么?
  • x86_64 世界上大多数人用于计算机的平台
  • @Mgetz 哦。我以前从未听说过它的名字。我通常听到 x86_64 或 amd64。
【解决方案2】:

TL;DR;

您可能无法将值强制进入编译单元,但您可以通过在标头中定义全局变量来强制使用符号。即:long using_my_library_version_1_2_3;

符号可以在最终的二进制文件中从外部访问,并且可以针对它进行测试(尽管与任何解决方案一样,它可以被规避,更不用说可以更改标头本身)。

编辑:为了澄清(由于评论),不要使用static 变量。

通过使用全局变量,它将默认为extern,并且不会被优化掉(以防其他加载二进制文件的对象使用该标识符)。

注意事项和示例:

如 cmets 中所述,全局变量的标识符(名称)此方法中的字符串。

但是,在编译可执行文件(和内核)时,使用 (-s) 编译时,可能会从最终二进制文件中删除标识符。这通常由嵌入式系统开发人员和喜欢调试的人执行(甚至更多)。

一个简单的例子:

// main.c
int this_is_example_version_0_0_1; /* variable name will show in the file */

int main(void) {
  /* placed anywhere to avoid the "not used" warning: */
  (void)this_is_example_version_0_0_1;
  return 0;
}

// extra.c
int this_is_example_version_0_0_1; /* repeat line to your heart's content  */
int this_is_example_version_0_0_1; /* (i.e., if header has no include guard) */

编译:

 $ cc -xc -o a -Wall -O2 main.c extra.c

列出所有标识符/名称(将显示全局):

 nm ./a | grep "this_is_example_version"

使用以下方法测试二进制文件中的字符串:

$ grep -F "this_is_example_version" ./a

详情:

使这个解决方案成为可能的关于 C 的有趣事实...:

  1. C 将 extern 定义为全局范围内函数和变量声明的默认值(6.2.2,第 5 小节)。

  2. 根据第 6.2.2 节(“标识符的链接”),“具有外部链接的特定标识符的每个声明都表示相同的对象或函数。”

    这意味着全局范围内的重复声明将被整理为单个声明。

  3. 当变量被放置在全局范围内并且它的所有位都设置为零时,变量声明和变量定义看起来相同。

    这是因为全局变量默认初始化为零。因此,编译器无法判断 int foo; 是定义 (int foo = 0;) 还是声明 (extern int foo;)。

由于这种“身份”和这些规则,编译器将模棱两可的全局变量声明/定义转换为“弱”声明,由链接器解析。

这意味着,如果您在没有 extern 关键字且没有值的情况下定义全局变量,则不明确的声明/定义将强制编译器发出弱符号,该符号将在最终二进制文件中公开。

此符号可用于标识在程序中某处使用了标头这一事实。

【讨论】:

  • 嗯。您的 TL;DR 基本上是 op 已经说过的,但没有 volatile,这使情况变得更糟。
  • @klutt volatile 如何让它变得更好?此外,OP 使用了一个静态变量,该变量将被优化掉。我的解决方案中提出的非静态extern(默认)变量不会被优化掉。
  • 我不知道具体是怎么回事,但根据 OP,volatile 对 gcc 有效,但对 clang 无效。
  • 此外,您正在使用 long 变量。这如何确保 OP 提到的字符串最终出现在二进制文件中?
  • @klutt - 在这种方法中,变量名 字符串。不是测试字符串对象,而是测试二进制输出的标识符。
【解决方案3】:

我不知道是否有任何标准的方法,但根据您的库的工作方式,我可能有一个合理的解决方案。许多库具有通常只在代码中调用一次或至少很少调用的 init 函数。 srand() 就是一个例子。

您可能需要一个 init 函数才能使您的库工作,并且没有具体说明它的用途,您可以只说在使用任何库函数之前主函数需要有 initlib(); 行。这是一个例子:

l.h:

// Macro disguised as a function
#define initlib() init("Frobnozzel v0.1; © 2019 ACME; GPLv");  

void init(const char *);
void libfunc(void);

l.c:

#include "l.h"
#include <string.h>
#include <stdlib.h>

int initialized = 0;

void init(const char *str) {
    if(strcmp(str, "Frobnozzel v0.1; © 2019 ACME; GPLv3") == 0)
        initialized = 1;
}

void libfunc(void) {
    if(!initialized)
        exit(EXIT_FAILURE);
    /* Do stuff */
}

注意: 我知道你只要求标题,但原理是一样的。毕竟,将 .h,.c 对转换为 .h 文件是世界上最简单的任务。

如果在使用初始化宏initlib之前使用库函数libfunc,程序将直接退出。如果在头文件中更改版权字符串,也会发生同样的事情。

当然,如果你愿意,解决这个问题并不难,但它确实有效。

为了测试,我使用了这段代码:

int main()
{
    initlib();
    libfunc();
    printf("Hello, World!\n");
}

我通过将l.c 编译到共享库中进行了尝试。然后我使用-O3 编译了一个带有clanggcc 的简单主程序。二进制文件可以正常工作,并且包含版权字符串。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-13
    • 2011-04-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多