通过无法优化的标头嵌入字符串

时间:2019-04-11 14:23:18

标签: c compiler-optimization copyright-display

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

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

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

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

我的小尝试等于:

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

...,但是在构建过程中很容易将其删除(由于实际未使用该字符串,因此对于优化编译器而言很容易被捕食)。

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

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


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

4 个答案:

答案 0 :(得分:3)

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

char

请注意,这是特定于GCC / C语的。

MSVC的替代方法是使用asm(".ascii \"Frobnozzel v0.1; © 2019 ACME; GPLv3\"\n\t"); #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,结果是相同的。

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

答案 1 :(得分:2)

TL; DR;

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

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

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

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

注意事项和示例:

如评论中所述,在这种方法中,全局变量的标识符(名称)

但是,在编译可执行文件(和内核)时,使用(-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关键字且不带值,则模棱两可的声明/定义将强制编译器发出一个弱符号,该符号将在最终二进制文件中公开。

此符号可用于识别标头已在程序中的某个地方使用的事实。

答案 2 :(得分:0)

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

您可能需要一个初始化函数来使您的库正常工作,而没有确切说明其用途,您可以说在使用任何库函数之前,主函数需要在行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文件是世界上最简单的任务。

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

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

为了进行测试,我使用了以下代码:

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

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

答案 3 :(得分:-3)

一个重要的规则是标头不得分配内存。这意味着标题:

  • 不得声明变量(除非以extern为前缀);
  • 不得定义函数(即不得生成代码);

您的声明:

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

与提到的规则冲突。


  

但是在构建过程中很容易删除

实际上,这并不是最糟糕的情况。最坏的情况将是两个以上的编译单元(.c文件)包含该标头。这些编译单元中的每一个都有一个名为frobnozzel_version_string的不同变量。如果编译器不会抱怨同名的不同变量,则由于无法确定要对哪个变量进行操作,因此对所述指针执行操作将导致令人惊讶的结果。


  

是否有可能在包含给定标头的二进制文件中嵌入一个字符串,而该字符串不会被构建“发布”二进制文件的常用策略优化/剥离?

如果您只想使字符串在任何地方都可见(前提是包含头文件),请使用:

#define FROBNOZZEL_VERSION_STRING ("Frobnozzel v0.1; © 2019 ACME; GPLv3")

它不分配内存,不能被模棱两可地使用...


@klutt 的评论使我更好地理解了这个问题:

  

OP希望的是,通过编译使用头文件的某些源生成的任何二进制文件都应自动包含版权字符串。

因此,您基本上想在所有包含该标头的编译单元中“注入”一些数据。

要防止编译器剥离数据,必须使用volatile

如何使用它是另一回事。我只看到一些替代方案:

  1. 仅标题:您将违反最佳实践规则(如上所述)。您将拥有许多相同数据的副本,但是只要不使用它,就不会有问题。

头文件

static volatile char frobnozzel_version_string[] = "Frobnozzel v0.1; © 2019 ACME; GPLv3";
  1. 提供一个代码库和标头:您将遵循最佳实践,但要付出两个文件的代价。

.c文件

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

头文件

extern static volatile char frobnozzel_version_string[]

staticvolatile中的order不重要。