将十六进制字符串转换为字节数组的预处理宏

4
我已经在我的集成开发环境中定义了一个AES-128密钥作为构建符号,这样它就会像这样调用GCC:
arm-none-eabi-gcc -D"AES_KEY=3B7116E69E222295163FF1CAA1681FAC" ...

这等同于 #define AES_KEY 3B7116E69E222295163FF1CAA1681FAC

好处是,这个符号也可以自动作为参数传递给一个后构建的 CLI 脚本,该脚本使用此密钥加密编译后的代码(例如用于安全固件更新)...

但是如何在代码中将此密钥存储为字节数组? 我想定义一个预处理器宏来进行转换:

uint8_t aes_key[] = { SPLIT_MACRO(AES_KEY) };

为了

uint8_t aes_key[] = {0x3B, 0x71, 0x16, 0xE6, 0x9E, 0x22, 0x22, 0x95, ...};

换句话说,GCC预处理器能否将关键字符串分成2个字符块,并在它们之间添加", 0x"?

4
不行,你需要生成源代码。预处理器可以将记号串联起来,但它不能将它们分开,而且它的功能相当有限。 - Petr Skocik
2
顺便问一下,你是指 uint8_t aes_key[] = {0x3B, Ox71, 0x16, 0xE6, 0x9E, 0x22, 0x22, 0x95, ...}(注意到[])对吧? - Stephan Lechner
3
使用任何其他工具(甚至是另一个C语言程序)创建一个.h文件,然后进行编译。所有操作都在makefile中进行。 - 0___________
1
@PSkocik 当然可以,但是字符串需要的ROM大小是uint8_t数组的两倍,再加上转换代码和存储字节密钥所需的RAM。对于嵌入式设备来说并不是理想的解决方案... - Motla
1
另一种选择可能是将参数设置为更适合 C 端的格式(例如字符串字面量,如 "\x3b\x71\x16..."),并更改其他后构建脚本以使其接受它。 - Matteo Italia
显示剩余5条评论
2个回答

5
有点笨拙,但如果你事先知道密钥的长度,可以按照以下步骤进行:
  1. 定义一个宏 HEXTONIBBLE,将十六进制数字转换为数字
  2. 定义一个宏 HEXTOBYTE,使用 HEXTONIBBLE 从十六进制中获取字节
  3. 使用 HEXTOBYTE 参数正确地初始化您的数组

如果您的 KEY 不是以字符串形式(即用双引号括起来)的,则可以使用 stringify 运算符 #(使用变参宏的技巧,以便在作为参数或另一个宏的参数时扩展宏):

//           01234567890123456789012345678901
#define K    3B7116E69E222295163FF1CAA1681FAC

#define STRINGIFY_HELPER(A) #A
#define STRINGIFY(...) STRINGIFY_HELPER(__VA_ARGS__)

#define KEY  STRINGIFY(K)

#define HEXTONIBBLE(c) (*(c) >= 'A' ? (*(c) - 'A')+10 : (*(c)-'0'))

#define HEXTOBYTE(c) (HEXTONIBBLE(c)*16 + HEXTONIBBLE(c+1))

uint8_t aes_key[] = {
    HEXTOBYTE(KEY+0),
    HEXTOBYTE(KEY+2),
    HEXTOBYTE(KEY+4),
    HEXTOBYTE(KEY+6),
    HEXTOBYTE(KEY+8),
    HEXTOBYTE(KEY+10),
    HEXTOBYTE(KEY+12),
    HEXTOBYTE(KEY+14),
    HEXTOBYTE(KEY+16),
    HEXTOBYTE(KEY+18),
    HEXTOBYTE(KEY+20),
    HEXTOBYTE(KEY+22),
    HEXTOBYTE(KEY+24),
    HEXTOBYTE(KEY+26),
    HEXTOBYTE(KEY+28),
    HEXTOBYTE(KEY+30)
};

int main() {

    for (int i=0; i<sizeof(aes_key); i++) {
        printf("%02X ", aes_key[i]);
    }

    return 0;
}

输出:

3B 71 16 E6 9E 22 22 95 16 3F F1 CA A1 68 1F AC 

2
这在C++中可以工作(尽管我不确定编译器是否需要在编译时评估它),但在C中不行:“test.c:13:22:错误:初始化元素不是常量”。 - Matteo Italia
1
https://ideone.com/pq3Zgy;我使用gcc 6.3在我的机器上得到了相同的结果;但是clang 4编译它没有问题。对我来说,这个错误并不意外——涉及字符串的操作(无论在编译时是否已知)从未被视为常量值。 - Matteo Italia
2
你可以将数组移动到函数内部使其正常工作,以解决此处违反的规则(静态存储期对象的初始化器中的所有表达式都应为常量表达式或字符串字面值,C99 §6.7.8 ¶4); 同样,请注意理论上这可能会延迟运行时的计算(尽管在实践中,gcc完全在编译时解析它)。 - Matteo Italia
2
好的,检查标准后,gcc和clang都是正确的;C99 §6.7.8 ¶4规定静态存储期对象的初始化必须是常量表达式(其想法是将它们的值直接嵌入可执行文件中);在§6.6 ¶7中解释了初始化程序实际上需要算术常量表达式(或NULL或地址常量+/-整数常量表达式);而算术常量表达式只能包含整数/字符/fp/枚举常量和sizeof表达式作为操作数,因此不能使用字符串字面量(虽然可以在地址常量中使用,但... - Matteo Italia
3
...只是为了推导出一个地址;不允许进行实际的解引用。因此,从技术上讲,gcc是正确的;clang的行为也是被允许的,因为第10段规定:“实现可以接受其他形式的常量表达式。”。然而,依赖于这一点当然会使代码不可移植。 - Matteo Italia
显示剩余5条评论

3
这并不回答原问题,但如果密钥可以使用这种格式编写:
#define AES_KEY 3B,71,16,E6,9E,22,22,95,16,3F,F1,CA,A1,68,1F,AC

以下宏定义即使在GCC下也有效:
#define BA(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p) {0x##a,0x##b,0x##c,0x##d,\
0x##e,0x##f,0x##g,0x##h,0x##i,0x##j,0x##k,0x##l,0x##m,0x##n,0x##o,0x##p}

#define TO_BYTEARRAY(...) BA(__VA_ARGS__)

uint8_t aes_key[] = TO_BYTEARRAY(AES_KEY);

请参见连接参数预扫描可变宏

可变参数宏的原因是什么?直接执行 BA(AES_KEY) 不起作用吗? - kutschkem
@kutschkem BA需要16个参数,而AES_KEY只代表一个,如果我们不使用可变宏。 - Motla

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