【问题标题】:How can I print the offset of a struct member at compile time?如何在编译时打印结构成员的偏移量?
【发布时间】:2021-09-16 12:54:20
【问题描述】:

给定一个结构,例如:

struct A {
    char a;
    char b;
} __attribute__((packed));

我希望在编译时打印结构中 b(在本例中为 1)的偏移量 - 我不想运行程序并调用类似 printf("%zu", offsetof(struct A, b)); 的东西,因为打印不是- 在我的平台上微不足道。我希望编译器本身打印偏移量,例如:

> gcc main.c
The offset of b is 1

我尝试了几种使用#pragma messageoffsetof 的方法,最接近的是:

#define OFFSET offsetof(struct A, b)
#define XSTR(x) STR(x)
#define STR(x) #x

#pragma message "Offset: " XSTR(OFFSET)

只打印:

> gcc main.c
main.c:12:9: note: #pragma message: Offset: __builtin_offsetof (struct A, b)

不打印数字偏移量。可以在编译时使用 _Static_assert 对偏移量进行二进制搜索 - 但我的真实结构很大,这可能会有点麻烦。

【问题讨论】:

  • 你不能写一个单独的测试程序来打印你需要的值吗?
  • 使用 GCC 和 Clang,您可以生成包含偏移量的“程序集”。例如,将__asm__("# offsetof(struct A, b) = %c0" : : "i" (offsetof(struct A, b))); 行放入函数中,编译为程序集(-S 开关),生成的程序集将包含## offsetof(struct A, b) = 1 之类的行。当然,您可以使用grep 从程序集中提取该行。
  • 为什么不能只检查链接器映射文件以查看结构最终有多大?
  • @nielsen 你在按钮上 - 我的主要收获是我对编译时消息的有限性感到有点惊讶(特别是考虑到像 _Static_assert 这样的功能存在)。不仅要打印偏移量,而且要打印 sizeof 等其他编译器信息似乎很困难。似乎最好的解决方案实际上是 hacky 解决方案,例如尝试通过编译器警告泄漏信息的答案,或者通过从编译的二进制文件中提取嵌入信息。
  • @hyde 我的具体情况是一些汇编代码需要用寄存器中结构的基地址取消引用结构的成员,我想要一种快速的方法来知道成员的偏移量跨度>

标签: c gcc


【解决方案1】:

我怀疑声明的约束“我希望编译器本身打印偏移量”是一个 XY 问题,我们只需要由用于构建的系统上的构建工具打印偏移量,而不是专门由编译器。

在这种情况下,GCC 和 Clang 能够 include arbitrary text in their assembly output 并在该文本中包含各种数据操作数,包括结构偏移的立即值。

在任何函数中,包括以下几行:

#if GenerateStructureOffsets
    __asm__("# offsetof(struct A, b) = %c0" : : "i" (offsetof(struct A, b)));
#endif

然后使用开关-DGenerateStructureOffsets-S 进行编译。编译器将生成一个名为<i>SourceFileName</i>.s 的文件,如果需要,您可以使用-o <i>Name</i> 为其命名。

然后grep "## offsetof" <i>Name</i> 会找到这一行,显示如下:

## offsetof(struct A, b) = 1

然后你可以使用sed或者其他工具来提取值。

__asm__ 中,"i" 表示生成“立即”操作数。后面的(offsetof(struct A, b)) 给出了它应该具有的值。在第一个带引号的字符串中,%c0 被替换为该操作数的值。

0 指示要替换的操作数——如果__asm__ 后面列出了多个操作数,它们的编号为 0、1、2、3,依此类推。 (还有一种命名它们而不是编号的机制,此处未显示。)通常,%0 将替换为适合目标汇编语言的立即操作数形式,例如$1#1。但是,c 修饰符表示要使用裸常量,因此替换文本只是值,在本例中为 1

【讨论】:

  • 我接受了 Luca 的回答,因为我认为它更直接地回答了问题本身(并展示了一个有用的 GCC 编译技巧)——但这是一个很好的解决方案,它与我最终使用的解决方案相似。
【解决方案2】:

给定这个宏:

#define PRINT_OFFSETOF(A, B) char (*__daniel_kleinstein_is_cool)[sizeof(char[offsetof(A, B)])] = 1

将其用于您的 main() 函数(或任何函数):

struct Test {
  char x;
  long long y;
  int z;
};

int main(void) {
  PRINT_OFFSETOF(struct Test, z);
  return 0;
}

你会得到这个警告:

warning: initialization of ‘char (*)[16]’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]

因此,offsetof(struct Test, z) == 16


注意:如果offsetof() 返回0(例如:PRINT_OFFSETOF(struct Test, x)),编译器警告将使用char (*)[] 而不是char (*)[16]

注意 2:我只使用 GCC 进行了测试。

【讨论】:

  • *__daniel_kleinstein_is_cool... 无耻 :)
  • @ryyker,卢卡正在寻求“公认的答案”;-)
  • 对我来说,这不太有效(公平地说,我正在运行旧版本的 gcc,7.5.0) - 它打印“initialization makes pointer from integer without a cast”。在编译器资源管理器上,我可以看到这确实适用于 gcc >= 8.1(dbush 的解决方案似乎适用于所有版本)。这仍然是一个非常好的解决方案,但存在生成编译器警告的明显缺点。
  • @LucaPolito - 您链接的页面与此相反。在这里玩得开心是完全可以的,只要它出现的数量比网站的其他 less fun 属性的数量要少。 :) 显然你的策略奏效了!
【解决方案3】:

令人惊讶的是,看起来 __builtin_choose_expr__deprecated__ 函数属性中起作用。以下程序:

#include <stddef.h>

struct A {
    char a;
    char b;
} __attribute__((packed));

#define printval_case(x, xstr, y, ...)  __builtin_choose_expr(x == y, xstr"="#y, __VA_ARGS__)
#define printval(x) do { \
    __attribute__((__deprecated__( \
        printval_case(x, #x, 0, \
        printval_case(x, #x, 1, \
        printval_case(x, #x, 2, \
        printval_case(x, #x, 3, \
        /* etc... */ \
        (void)0 )))) \
    ))) void printval() {} \
    printval(); \
} while (0)

int main() {
    printval(offsetof(struct A, a));
    printval(offsetof(struct A, b));
}

编译后,gcc会输出:

<source>:23:30: warning: 'printval' is deprecated: offsetof(struct A, a)=0 [-Wdeprecated-declarations]
<source>:24:30: warning: 'printval' is deprecated: offsetof(struct A, b)=1 [-Wdeprecated-declarations]

以类似的方式,您可以将值嵌入到可执行文件中,(类似于 CMake detects compiler stuff):

#include <stddef.h>
struct A {
    char a;
    char b;
} __attribute__((packed));
#define printval_case(x, xstr, y, ...)  __builtin_choose_expr(x == y, xstr"="#y, __VA_ARGS__)
#define embedval(x) do { \
    static const __attribute__((__used__)) const char unused[] = \
        printval_case(x, #x, 0, \
        printval_case(x, #x, 1, \
        printval_case(x, #x, 2, \
        printval_case(x, #x, 3, \
        /* etc... */ \
        (void)0 )))); \
} while (0)
int main() {
    embedval(offsetof(struct A, a));
    embedval(offsetof(struct A, b));
}

然后:

$ gcc file.c && strings ./a.out | grep offsetof
offsetof(struct A, b)=1
offsetof(struct A, a)=0

【讨论】:

  • 好招!我必须给 gcc -fcompare-debug-second 选项以避免被编译器的(不是那么)有用的注释发送垃圾邮件。
  • 是的。但是宏 printval_case( 是为了可读性而存在的,可以删除并内联 - 然后,它将只是一个 in expansion of 消息。
【解决方案4】:

也许可以接受新的预处理步骤。这可以作为一个单独的步骤完成,不会影响您的生产二进制文件。

offsetdumper.sh

#!/bin/bash
#
# pre-process some source file(s), add a macro + main() and a file with rules
# describing the interesting symbos. Compile and run the result.

dumprulefile="$1"
shift

# Define your own macros, like OFFSET, in the "Here Document" below:
{
gcc -E "$@" && cat<<EOF
#define OFFSET(x,y) do { printf("%s::%s %zu\n", #x, #y, offsetof(x,y)); } while(0)
#include <stddef.h>
#include <stdio.h>
int main() {
EOF
cat "$dumprulefile"
echo '}'
} | g++ -x c - && ./a.out

rules

OFFSET(A,a);
OFFSET(A,b);

source.h

typedef struct {
    char a;
    char b;
} __attribute__((packed)) A;

例子:

$ ./offsetdumper.sh rules *.h
A::a 0
A::b 1

这有点脆弱,如果您的 source.h 包含 main 函数,这将无法工作,因此可能需要进行一些修改才能满足您的需求。

【讨论】:

  • 这种方法的问题是它假设您正在本地编译。从 OP 的描述来看,他似乎在为嵌入式设备进行交叉构建,您不能假设结构布局在不同的目标上是相同的。
  • @plugwash 如果是这种情况,则需要交叉编译并在目标上执行。有点麻烦,但通常可行。
【解决方案5】:

一种可能的方法是将偏移量设置为数组的大小,然后将该数组的地址传递给期望不兼容指针的函数,以便打印类型:

static int a[offsetof(struct A, b)];
static void foo1(int *p) { (void)p; }
static void foo2(void) { foo1(&a); }

打印出来:

x1.c: In function ‘foo2’:
x1.c:13:1: warning: passing argument 1 of ‘foo1’ from incompatible pointer type [enabled by default]
 static void foo2(void) { foo1(&a); }
 ^
x1.c:12:13: note: expected ‘int *’ but argument is of type ‘int (*)[1]’
 static void foo1(int *p) { (void)p; }
             ^

【讨论】:

  • 一个巧妙的技巧,但我想当偏移量为 0 时它会中断。另外,我猜 OP 想要一条低于警告级别的消息。
  • 有趣的破解。 :-) 你可以给函数起一个口号,既可以作为一个很好的例子,也可以让输出说明打印的内容。
猜你喜欢
  • 2023-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-26
  • 2011-01-03
相关资源
最近更新 更多