11

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

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

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

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

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

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

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

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

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


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

4

3 回答 3

4

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

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

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

MSVC 的替代方法是使用#pragma commentor __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或创建将字符串嵌入可执行文件的资源文件。因此,链接时字符串将被删除。

于 2019-07-11T14:47:00.827 回答
2

TL;博士;

您可能无法将值强制进入编译单元,但您可以通过在标头中定义全局变量来强制使用符号。IE: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关键字和值的全局变量,模棱两可的声明/定义将迫使编译器发出一个弱符号,该符号将在最终的二进制文件中公开。

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

于 2019-07-10T17:26:49.117 回答
0

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

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

lh:

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

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

液晶:

#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我通过编译成一个共享库来尝试这个。然后我编译了一个简单的主程序,clang同时gcc使用-O3. 二进制文件可以正常工作,并且包含版权字符串。

于 2019-07-10T13:40:05.120 回答