当长度超过 0xF 时,硬编码字节数组将作为数据结束

Hardcoded byte array ends up as data when it exceeds 0xF in length

我的目标是让我的 .rdata 部分包含尽可能少的内容,并让编译器尽可能使用 text/code 部分。现在我有一个小问题,希望有人能帮助我。在 clang 和 GCC 中,编译以下 C++ 代码时(注意数组长度为 15 个字节):

#include <windows.h>

void _start() {
  unsigned char bytes[] = {0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF};
  MessageBoxA(nullptr, (char*)bytes, "Hi", MB_OK);
}

这编译如我所愿。所有硬编码数据都很好地嵌入到代码本身(它使用立即移动),因此不会向任何数据部分添加任何内容,也不会引用任何数据部分。下面是 IDA PRO 反编译:

如果您要向数组添加另一个字节并使其长度超过 15 个字节(现在总长度为 16 个字节),如下所示:

#include <windows.h>

void _start() {
  unsigned char bytes[] = {0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10};
  MessageBoxA(nullptr, (char*)bytes, "Hi", MB_OK);
}

然后编译器以我想要摆脱的方式运行。那就是它不再将它嵌入代码中,而是将所有内容移动到数据部分(unk_100402000,在 .rdata 内):

我针对多个编译器测试了这段代码。 MSVC 总是将这些硬编码的字节数组很好地嵌入代码本身,无论代码有多长。因为我真的很想继续使用Clang和GCC,所以我希望有一个解决方案。

我需要限制 .rdata 部分的使用的原因是我正在做一个实验项目,在 text/code 部分之外的任何引用或访问都会导致重大的性能损失。 text/code 部分之外的每次访问速度大约慢 1000 倍。因此,我真的需要让编译器尽可能多地使用 text/code 部分。

问题

如何让 Clang/GCC 编译 15+ 长的硬编码字节数组,以便它在堆栈上使用立即 mov 操作而不是使用 .rdata 部分?如果无法通过编译器选项实现,是否可以通过编译器传递来更改此行为?我也可以接受任何我需要应用到 Clang 才能完成这项工作的肮脏技巧。

我知道我可以将较长的字节数组拆分成较小的单个数组,但这不是我正在寻找的解决方案。

在此先感谢您提供的任何帮助!

Clang 版本:4.0.1

当前最佳解决方案

如下图所示。大的硬编码字节数组被编译为堆栈上的多个立即 mov 指令,因此从不引用数据部分中的任何内容。这正是我要找的。是否有多种方法可以实现相同的行为?

如果数据是静态的(不是函数作用域的局部数据),您可以执行以下操作:

 void Check( unsigned const char* x);

 void Do() {
   static const unsigned char bytes[] __attribute__((section(".text"))) =
   {
       0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
       0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
       0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
       0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
       0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
       0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
       0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
       0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10
   };
   Check( bytes );
 }

 int main()
 {
     Do();
 }

最终出现在:

Disassembly of section .text:
     08048580 <Do()>:
  8048580:  83 ec 18                sub    [=11=]x18,%esp
  8048583:  68 a0 85 04 08          push   [=11=]x80485a0
  8048588:  e8 e3 ff ff ff          call   8048570 <Check(unsigned char const*)>
  804858d:  83 c4 1c                add    [=11=]x1c,%esp
  8048590:  c3                      ret

  ... much other disassembled code follows ...

 080485a0 <Do()::bytes>:
  80485a0:  01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10     ................
  80485b0:  01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10     ................
  80485c0:  01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10     ................
  80485d0:  01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10     ................
  80485e0:  01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10     ................
  80485f0:  01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10     ................
  8048600:  01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10     ................
  8048610:  01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10     ................

所有内容仅在 .text 部分。

如果数据必须是本地的,您可以使用以下方法简单地手动复制:

void Check( unsigned const char* x); 

template< typename IN, typename OUT, unsigned int size >
void Cpy( IN (&in)[size], OUT (&out)[size] )
{   
    static_assert( sizeof( IN) == sizeof( OUT ) );
    memcpy( out, in, size * sizeof(OUT) );
}   

void Do() {
  static const unsigned char static_bytes[] __attribute__((section(".text"))) = 
  {   
      0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
      0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
      0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
      0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
      0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
      0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
      0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10,
      0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10
  };  

  unsigned char bytes[ sizeof( static_bytes )]; 
  Cpy( static_bytes, bytes );


  Check( bytes );
}   

int main()
{   
    Do();
}

所有内容仍在 .text 部分中,但您必须自己将数据复制到本地上下文中。我无法强制编译器本身这样做。

如果你能承受性能损失,像这样的东西似乎对 clang 有效;在我的测试中,它编译成一组直接的 mov(gcc 在修复属性后也可以工作):

template <unsigned char C>
struct noopt { [[clang::optnone]] unsigned char operator()() const { return C; } };

#define NODATA(v) (noopt<(v)>{}())

int main(){
    unsigned char bytes[] = { NODATA(0x1), NODATA(0x2), NODATA(0x3), NODATA(0x4), };

    // ...
}

这还有在任何地方工作的好处(不仅仅是在数组声明中)。

我使用编译器资源管理器在多个版本的 gcc 和 clang 上测试了以下内容,它似乎有效。

cout只是为了防止编译器将其优化为nop。

#include <iostream>
#include <cstring>

void f(int num) {
    char r0[15] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
    char r1[15] = {1,2,9,4,5,6,7,3,9,10,11,12,13,14,15};
    char r2[14] = {3,9,2,3,4,5,6,7,8,9,10,11,12};

    char r[44];

    std::memcpy(r, r0, 15);
    std::memcpy(r + 15, r1, 15);
    std::memcpy(r + 30, r2, 14);

    std::cout << r;
}