如何在GCC中指定枚举类型的大小?

9
我希望为一个枚举类型指定64位的大小,如何在GCC中实现?代码不需要“可移植性”,因为我只想让代码在编译为x86-32和x86-64 Linux的GCC上运行。这意味着只要能够提供所需功能的任何技巧都可以接受,只要它对这些目标有效即可。
给定以下代码:
#include <stdlib.h>
#include <stdio.h>

enum some_enum
{
    garbage1,
    garbage2
};

int main(void)
{
    enum some_enum some_val;
    printf("size: %lu\n", sizeof(some_val));

    return EXIT_SUCCESS;
}

目前它会打印出4,但我希望能够强制大小为8。在枚举分配中尝试指定大于4个字节的值会引发警告。例如:

enum some_enum
{
    garbage1 = '12345',
    garbage2
};

会产生:

warning: character constant too long for its type [enabled by default]

这里有一个类似问题的答案,但似乎没有给出好的结果。也就是说,以下代码会产生相同的警告:
enum some_enum
{
    garbage1 = 'adfs',
    garbage2 = 'asdfasdf'
};

注意:可以通过编译-Wno-multichar来关闭多字符警告。

原理

由于人们对我为什么这样做感到兴趣,我编写了一个反汇编引擎。我将指令的每个部分作为字符串获取。因此,我希望枚举看起来像这样:

enum mnemonic
{
    mov = 'mov',
    cmp = 'cmp',
    sysenter = 'sysenter'
};

我可以使用以下代码轻松地存储语义信息: ```python 我能够使用如下代码轻松地存储语义信息: ```
enum mnemonic insn;

char *   example_insn = "mov";
uint64_t buf          = 0;

strncpy((char *)&buf, example_insn, sizeof(uint64_t));

如果buf是一个枚举助记符,那么我们不需要做任何其他的事情。 strncpy用于将字符串结束后的字节填充为null字符。如果我无法做到这一点,我将不得不执行以下操作:
if(strcmp(example_insn, "mov") == 0) {
    insn = mov;
} else if(strcmp(example_insn, "cmp") == 0) {
    insn = cmp;
} ...

由于这个例程会被执行数百万次,这种优化会有很大的影响。我也打算对诸如寄存器等操作数进行相同的优化。


3
为什么你想这样做?听起来好像你没有按照“正常”的方式使用enum - Dan Fego
3
“12345”代表什么?是一个包含5个字符的单个字符吗?如果要存储数值,请不要加上 ' - 在x64上使用 gcc 不会收到任何警告。 - Matteo Italia
1
JFTR,'ABC' 是某些非标准的 MSVC 扩展,用于 'A'<<(u*CHAR_BIT) | 'B'<<(v*CHAR_BIT) | 'C'<<(w*CHAR_BIT)...,其中 u、v、w 等与实数集之间存在着实现定义映射。 - jørgensen
1
@DanFego:我添加了一个解释。 - Mike Kwan
@KeithThompson:抱歉,我不知道多字符字面量,这是我第一次遇到它们。 - Matteo Italia
显示剩余5条评论
9个回答

8
您可以使用一个 union 类型:
union some {
    enum { garbage1, garbage2 } a;
    int64_t dummy;
};

不幸的是,这似乎是最好的方法。在我的情况下,我还不得不添加一些填充,而不是使用匿名结构体,以便我的编译器的2字节枚举得到正确对齐。我认为Gnu真的需要添加一些枚举属性。 - Jim Fell
不幸的是,这似乎是最好的方法。在我的情况下,我还不得不添加一些填充,通过使用匿名结构体,以便我的编译器的2字节枚举能够正确对齐。我认为GNU真的需要添加一些枚举属性。 - Jim Fell

8

正如Matteo Italia的回答所说,gcc允许你通过为其中一个成员指定64位值来定义64位枚举类型。例如:

enum some_enum {
    /* ... */
    max = 0x7fffffffffffffff
};

关于您使用的'mov'、'cmp'等指令,字符串字面值如"mov"和多字符字符常量如'mov'之间并没有必然的关联。
后者是合法的(并且被gcc支持),但是其值是实现定义的。标准规定其类型始终为int,而gcc似乎没有扩展可以覆盖此规定。因此,如果int是4个字节,则'sysenter'(如果被接受)的值可能不是您要查找的值。gcc似乎只考虑这种常量的低位字节。该常量的值似乎在大端和小端系统上保持一致,这意味着它不会与类似字符串字面值的表示方式一致。
例如,以下程序:
#include <stdio.h>
int main(void) {
    const char *s1 = "abcd";
    const char *s2 = "abcdefgh";
    printf("'abcd'     = 0x%x\n", (unsigned)'abcd');
    printf("'abcdefgh' = 0x%x\n", (unsigned)'abcdefgh');
    printf("*(unsigned*)s1 = 0x%x\n", *(unsigned*)s1);
    printf("*(unsigned*)s2 = 0x%x\n", *(unsigned*)s2);
    return 0;
}

使用gcc在小端系统(x86)上编译时,将会产生以下输出:

'abcd'     = 0x61626364
'abcdefgh' = 0x65666768
*(unsigned*)s1 = 0x64636261
*(unsigned*)s2 = 0x64636261

在大端系统(SPARC)上,它会输出以下内容:

'abcd'     = 0x61626364
'abcdefgh' = 0x65666768
*(unsigned*)s1 = 0x61626364
*(unsigned*)s2 = 0x61626364

所以,我很抱歉你的想法,即将字符常量(如'mov')与字符串(如"mov")进行匹配,是行不通的。(可以想象,你可以将字符串表示规范化为大端,但我不会采用这种方法。)你试图解决的问题是快速地将字符串(如"mov")映射到表示CPU指令的特定整数值上。你是对的,长序列的strcmp()调用将是低效的(你实际上测量过并发现速度不可接受吗?),但有更好的方法。某种哈希表可能是最好的。有工具可以生成完美的哈希函数,因此对字符串值进行相对便宜的计算即可得到唯一的整数值。你将无法像方便地编写枚举值的定义,但一旦你拥有正确的哈希函数,你可以编写一个程序来生成enum类型的C源代码。这是假设枚举是最好的方法;这可能不是最好的方法。如果我在做这个,中心数据结构将是包含操作符的字符串名称和任何其他相关信息的structs集合。哈希函数将字符串(如"mov")映射到该集合中的索引。(我故意模糊了要使用哪种"collection"的类型;使用正确的哈希函数,它可能是一个简单的数组。)有了这种解决方案,我认为不需要64位的enum类型。

关于是否可以使用哈希表更快地完成此操作,我真诚地认为不行。我现在正在使用来自Linux内核的哈希表。它需要一个键和一个键的大小。这意味着我实际上仍然需要做我目前正在做的填充。原因是我实际上简化了理由 - 我从libopcodes生成的字符串(我用于反汇编)具有需要去除的空格字符填充。使用哈希表将使用完全相同的计算量,但还会增加计算哈希和添加到哈希表的成本。 - Mike Kwan
经过一些调查,我同意实际问题是多字符文字只能是4个字节(至少在x86-32和x86-64下)。然而,我找不到任何引文来证明这一点。Per Johansson的解决方案有效。 - Mike Kwan

6
尽管C99标准规定枚举只能基于int类型(§6.7.2.2 ¶2)1,但似乎gcc遵循C++的想法,如果枚举中的值大于int,它可以基于更大的整数类型。我对这段代码没有任何问题,在x86和x64上都没有问题。
enum myEnum
{
    a=1234567891234567890LL
};
 
int main()
{
    enum myEnum e;
    printf("%u %u", sizeof(void *), sizeof(e));
    return 0;
}

在x86上我得到了

4 8

在 x64 上(在我的计算机上)我得到

8 8

虽然我要求严格遵守标准,但正如预期的那样:

matteo@teodeb:~/cpp$ gcc -ansi -pedantic testenum.c
testenum.c:5:7: warning: use of C99 long long integer constant
testenum.c:5: warning: ISO C restricts enumerator values to range of ‘int’

实际上,情况要复杂一些;第4段指定实现可以选择任何与char、有符号整数类型或无符号整数类型“兼容”的“基础类型”作为基础类型,只要它可以表示enum的所有元素。另一方面,第2段指定enum的每个成员必须可表示为int,因此,即使实现可以基于一个亿万位整数构建你的enum,为其定义的常量也不能是int无法表示的内容。因此,这意味着实际上编译器不会将enum基于比int更大的类型,但如果你的值不需要int的全部范围,则可能会将其基于更小的类型。感谢@jons34yp指出我的初始错误。

尽管有这个警告,但这个解决方案是否保证在我的目标平台上按预期工作?此外,是否可以将其适应我想要的字符串/多字符文字样式?我猜我总是可以生成那些数字,但这样不太易读。 - Mike Kwan
@MikeKwan:只有在明确要求严格遵守标准(-ansi -pedantic)时,警告才会出现。就gcc而言,如果需要将enum基于比int更大的类型,则没有问题(正如您所见,我在x86和x64上测试过,它可以正常工作)。至于多字符文字,我正在尝试查看是否有办法使它们正常工作。 - Matteo Italia
如果你特别想要64位的枚举,可以这样做:enum some_enum { /* ... */ max = 0x7fffffffffffffff };。拥有该值的成员保证了该类型需要(至少)64位(或者编译器会拒绝它)。 - Keith Thompson
@KeithThompson:0x7fffffffffffffffLL,我想是这样。至于多字符字面量的问题,我认为没有解决办法。 - Matteo Italia
@MatteoItalia。你的答案是错误的。§6.7.2.2 ¶2 只讨论枚举常量(即 a = 12345 部分),而 §6.7.2.2 ¶4 则说:“每个枚举类型<...>。类型的选择是由实现定义的。” - user283145
显示剩余2条评论

5

您误解了这个警告,它的意思是字符字面量始终是int类型,而不是longlong long类型。

您可以尝试以下方法:

enum foo {
    garbage1 = (long long)'1' << 32 | (long long)'2' << 24 | (long long)'3' << 16 | (long long)'4' << 8 | (long long)'5',
    garbage2
};

但如果您想使用strncpy解决方案,仍需小心使用小端序,确保使用正确的位移数。


这是一个不错的解决方案。我稍后会尝试一下。你知道这能不能以某种方式转换成宏吗? - Mike Kwan
这个解决方案完美地运作。我将接受这个答案并写一个简短的说明,展示如何使用它。 - Mike Kwan

2

Per Johansson在他的这个回答中准确地指出了问题所在。作为如何使用该技术的具体示例,我编写了这个程序(insn_enum.c):

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <inttypes.h>

enum insn {
    /*
     * Have the characters backwards because C treats the value as an
     * integer (of size 64  bits in this case). There is no need for
     * a null terminator since we are treating the values as an integer,
     * not a string.
     */
    sysenter = (uint64_t)'r' << 56 | (uint64_t)'e' << 48 |
            (uint64_t)'t' << 40 | (uint64_t)'n' << 32 |
            (uint64_t)'e' << 24 | (uint64_t)'s' << 16 |
            (uint64_t)'y' << 8 | (uint64_t)'s',
};

int main(void)
{
    enum insn some_insn = sysenter;
    char * insn = "sysenter";

    uint64_t val = 0;

    /*
     * We can optimise this by traversing backwards (little endian) setting
     * 0 till a NULL char is found, although I will not bother implementing
     * this till I have done some profiling.
     */
    strncpy((char * )&val, insn, sizeof(uint64_t));

    printf("size: %" PRIuPTR"\n", sizeof(enum insn));

    if(some_insn == val) {
        puts("Works");
    } else {
        puts("Doesn't work");
    }

    return EXIT_SUCCESS;
}

这可以通过以下 makefile 进行编译:

all:
    gcc -std=gnu99 -m32 -Wall insn_enum.c -o insn_enum_32
    gcc -std=gnu99 -m64 -Wall insn_enum.c -o insn_enum_64

clean:
    rm -f insn_enum_32
    rm -f insn_enum_64

运行 ./insn_enum_32 && ./insn_enum_64 命令会打印如下内容:

size: 8
Works
size: 8
Works

需要注意的是,这仅表明我们可以在x86-32和x86-64(我打算针对的唯二平台)上使此技巧起作用。实际上,由于语言处理枚举类型的方式,这个技巧保证在大端系统上不起作用。此外,即使我们按照已经规定的方式指定了uint64_t作为enum的大小,也不能保证编译器一定会使用该大小。事实上,使用-pedantic进行编译将会给出警告:

gcc -std=gnu99 -m32 -pedantic -Wall insn_enum.c -o insn_enum_32
insn_enum.c:13:13: warning: ISO C restricts enumerator values to range of ‘int’
gcc -std=gnu99 -m64 -pedantic -Wall insn_enum.c -o insn_enum_64
insn_enum.c:13:13: warning: ISO C restricts enumerator values to range of ‘int’

1

仅回答标题中的原始问题-C++11允许您指定枚举的类型,因此也确定了其大小:

enum class mynamedenum : long {
  FOO,
  BAR
}

这个回答说的是C++,而问题明确标记为C... - Simon Sobisch

1

目前还没有官方的方法来指定enum的大小。也许不完全适用于您的情况,但是通过这个技巧,您可以实现一些目标enum大小以实现跨平台。您应该使用关键字__attribute__和参数packed来将enum的大小缩小到枚举列表中的最大值。

只需查看以下示例:

#include <stdio.h>

typedef enum  __attribute__((packed))
{
  MY_E_8_BYTES_MAX = 0xFFFFFFFFFFFFFFFF,
} en_8b_t;

typedef enum  __attribute__((packed))
{
  MY_E_4_BYTES_MAX = 0xFFFFFFFF,
} en_4b_t;

typedef enum  __attribute__((packed))
{
  MY_E_2_BYTES_MAX = 0xFFFF,
} en_2b_t;

typedef enum  __attribute__((packed))
{
  MY_E_1_BYTE_MAX = 0xFF,
} en_1b_t;

typedef enum
{
  MY_E_X_BYTES_DEFAULT = 0,
} en_xb_t;

int main(int argc, char ** argv)
{
  printf("Sizeof en_8b_t: %lu\n", sizeof(en_8b_t));
  printf("Sizeof en_4b_t: %lu\n", sizeof(en_4b_t));
  printf("Sizeof en_2b_t: %lu\n", sizeof(en_2b_t));
  printf("Sizeof en_1b_t: %lu\n", sizeof(en_1b_t));
  printf("Default enum size is: %lu\n", sizeof(en_xb_t));

  return 0;
}

输出看起来像这样:

Sizeof en_8b_t: 8
Sizeof en_4b_t: 4
Sizeof en_2b_t: 2
Sizeof en_1b_t: 1
Default enum size is: 4

默认枚举大小取决于您的编译器。 如需更多属性,请查看此处


这是一个好建议,尽管它可能只适用于GCC编译器。 - EthanL
是的,这个问题是针对GCC提出的。这些属性是针对不同编译器的特定属性。 - Dmytro Kryvyi

0

也许你可以使用宏定义?

#define GARBAGE1 12345L
#define GARBAGE2 67890L

我认为你不能使用不同于最优大小的枚举。

也许尝试一下:

enum
{
 garbage1,
 garbage2,
 sentinel = 12345L
}

看看呢?


#define 是我的最后选择。我更喜欢枚举。你给出的枚举建议并不完全符合我的要求。我需要它表示一个字符串。 - Mike Kwan

0

你最好的选择可能是使用构建系统自动生成一组定义。这样,你也可以正确地获取字节序。

一个示例程序gen-instructions可能看起来像这样

#include <inttypes.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    for(int i = 1; i < argc; ++i)
    {
        uint64_t value;
        strncpy((char *)&value, argv[i], sizeof value);
        printf("#define %s 0x%.16" PRIX64 "\n", argv[i], value);
    }

    return 0;
}

使用相应的makefile规则

instructions.h : instructions.list gen-instructions
    ./gen-instructions `cat $<` > $@

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