【问题标题】:Pre-processor macro to convert an hex string to a byte array将十六进制字符串转换为字节数组的预处理器宏
【发布时间】:2018-02-01 14:00:24
【问题描述】:

我在我的 IDE 中定义了一个 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”?

【问题讨论】:

  • 没有。您应该生成源。预处理器可以连接标记但不能拆分它们,而且它完全是有限的。
  • 顺便说一句:你的意思是uint8_t aes_key[] = {0x3B, Ox71, 0x16, 0xE6, 0x9E, 0x22, 0x22, 0x95, ...}(注意[]),对吧?
  • 使用任何其他工具(甚至是 C 中的另一个程序)创建一个 .h 文件,然后编译。全部在makefile中
  • 另外,您可以将字符串拆分为整数大小的块,或者您可以简单地将字符串用作字符串并在运行时对其进行解析,将各个字节保存到数组中。除非您有成千上万个这样的字符串,否则在运行时花费的几个额外周期应该不会引起注意。
  • 或者只是一个函数,为什么不在一个函数中初始化数组呢?您不需要动态分配它。

标签: c gcc macros c-preprocessor preprocessor


【解决方案1】:

有点笨拙,但是如果您提前知道密钥的长度,您可以按如下方式处理它:

  1. 定义一个宏 HEXTONIBBLE 将十六进制数字转换为数字
  2. 定义一个宏 HEXTOBYTE,它使用 HEXTONIBBLE 从十六进制中获取一个字节
  3. 使用正确参数化的HEXTOBYTE 初始化您的数组

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

//           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 

【讨论】:

  • 这适用于 C++(尽管我不确定编译器是否需要在编译时对其进行评估),但不适用于 C:"test.c:13:22: error: initializer元素不是常数”。
  • ideone.com/pq3Zgy;在我的机器上使用 gcc 6.3 的结果相同; clang 4 可以很好地编译它。对我来说,这个错误并不意外 - 涉及字符串的操作(无论在编译时如何知道)从未被视为常量值。
  • 您可以让它在函数内移动数组,以解决此处违反的规则(具有静态存储持续时间的对象的初始化程序中的所有表达式都应为常量表达式或字符串文字。,C99 §6.7.8 ¶4);再次注意,理论上这可能会延迟运行时的计算(尽管实际上 gcc 在编译时会完全解决它)。
  • 好的,所以,检查gcc和clang的标准是对的; C99 §6.7.8 ¶4 要求使用常量表达式来初始化静态存储持续时间对象(想法是它们的值应该直接烘焙到可执行文件中);在 §6.6 ¶7 中解释了初始化器实际上需要 算术常量表达式(或 NULL 或地址常量 +/- 整数常量表达式);算术常量表达式反过来可能只包含整数/字符/fp/enum 常量和 sizeof 表达式作为操作数,所以没有字符串文字(可以在地址常量中使用,但是 ...
  • ... 仅用于派生地址;不允许 实际 取消引用)。所以,gcc 在技术上是正确的; clang 的行为也是允许的,正如 ¶10 所说的“一个实现可以接受其他形式的常量表达式。”。但是,依赖于此当然会使代码不可移植。
【解决方案2】:

这并没有回答原始问题,但如果密钥可以用这种格式编写:

#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);

ConcatenationArgument PrescanVariadic Macros

【讨论】:

  • 可变参数宏是什么原因?直接做BA(AES_KEY)不行吗?
  • @kutschkem BA 接受 16 个参数,而如果我们不使用可变参数宏,AES_KEY 只表示一个。
猜你喜欢
  • 2021-10-31
  • 2019-02-12
  • 1970-01-01
  • 2017-06-17
  • 2013-01-14
相关资源
最近更新 更多