可以用单个指针将各种类型的值写入内存地址吗?

Possible to write various-typed value to a memory address with a single pointer?

我把内存当作一种学术练习,并尝试像在汇编中一样将值写入内存,基本上是给它一个大小 (1,2,4,8)以及一个值和一个内存地址,例如:

movb ,   -1(%rbp)      size=1; value=7,   address=-1 (pretend rbp=0)
movw 7, -4(%rbp)      size=2; value=777, address=-4

在 C 中执行此类操作时,我是否需要创建多个不同大小的指针来执行此类随机大小的写入?例如,目前我是这样做的:

char* play_mem_block(size_t bytes)
{
    char *mem_block = malloc(bytes);
    if (!mem_block) {
        perror("Looks like there was an error");
        return NULL;
    }

    // write a one-byte char value at address0
    *mem_block = 'a';

    // write a four-byte (int) value
    int *mem_block_for_int = (int*) mem_block;
    *(mem_block_for_int + 1) = 4444;

    /* *(mem_block+3) = 4444; */     <-- possible to do this directly?

    return mem_block;

}

这是一个更接近您的目标的改编:

char* play_mem_block(size_t bytes)
{
    char *mem_block = malloc(bytes);
    if (!mem_block) {
        perror("Looks like there was an error");
        return NULL;
    }

    // write a one-byte char value at address 0
    char a = 'a';
    memcpy(mem_block, a, sizeof(a));

    // write a four-byte (int) value
    int ffffour = 4444;
    memcpy(mem_block + 1, &ffffour, sizeof(ffffour));

    return mem_block;
}

在实践中,您需要创建某种用于分配的“缓冲区”处理程序,然后是像这样的辅助方法:

void* write_int32_t(void* b, int32_t n) {
  memcpy(b, &n, sizeof(n));

  return b + sizeof(n);
}

您可以在其中为需要写出的每种类型消除变体。你也可以写一个反 read_int32_t 方法:

void* read_int32_t(void* b, int32_t* n) {
  memcpy(n, b, sizeof(n));

  return b + sizeof(n);
}

这同样微不足道。

你可以这样做:

*(int *)(mem_block + 4) = 4444;    // only safe for aligned offsets

((int *)mem_block)[1] = 4444;

为了写成 int 你需要一个类型为 int 的表达式来写,因此需要强制转换。

注意对齐,某些系统有对齐要求,尝试在 3 字节的偏移处写入 int 可能未定义,具体取决于平台。要安全地进行未对齐的写入,您需要使用 memcpy 版本。

您原来的方法很好,而且可以说更容易阅读,所以不要急于为此做一些不同的事情。

像这样的 Asm 就是 C 结构的工作方式;转换为具有您想要的布局的 struct foo* 是一种选择。

如果您想对不同类型进行重叠访问,请注意 C 中的严格别名和对齐规则。 C 是不是 可移植的汇编语言;如果您不希望编译器在编译时“破坏”您的代码,则必须遵守 C 语言的规则。 (或者来自另一个 PoV,因为你的代码不是你想要的,也不是已经被破坏了。)例如 表明即使在像 x86-64 这样的 ISA 上,编译器优化后也可能出现与对齐相关的段错误,其中正常标量未对齐 loads/stores 永远不会出错。

对未对齐的 4 字节存储使用 memcpy(ptr+3, &u32value, sizeof(u32value));,您可以使用不同的类型安全地重新加载它。 (对于像 uint32_t u32value = 1234; - uin32_t 这样的 var 类型,如果系统提供的话,required 正好是 32 位宽且没有填充。)

有了一个不错的现代编译器,目标是 ISA(如 x86-64),其中未对齐的加载很便宜,memcpy 将相当可靠地内联并优化为具有正确操作数的单个 mov 存储尺寸。

*(uint32_t)(ptr+3) = 1234 not 是安全的,因为 alignof(uint32_t) 在大多数 C 实现(具有 8 位字符的实现)上是 4。由于 malloc 本身 returns 内存充分对齐以存储任何类型(最多 max_align_t),ptr+3 肯定是未对齐的(除了在具有宽字符的奇怪系统上,其中 sizeof(uint32_t) =1,或者它选择不需要对齐更宽类型的地方)。


或者在 GNU C 中我们可以使用类型属性来告诉编译器类型的欠对齐版本,and/or 给它们相同的别名能力char*.

__m128i 和其他 Intel 向量内部类型使用 may_alias 属性;这就是编译器如何确保 _mm_load_ps( (float*)ptr_to_int ) 和类似的东西安全。)

typedef uint32_t unaligned_aliasing_u32 __attribute__((aligned(1),may_alias));


char *ptr = malloc(1234);

*(unaligned_aliasing_u32)(ptr+3) = 5678;   // movl 78, 3(%rax)
*(unaligned_aliasing_u32)(ptr+5) = 5678;   // movl 78, 5(%rax) overlapping 
int tmp = ((const uint32_t*)ptr)[1];       // mov 4(%rax), %edx   aligned so I used plain uint32 for example.

请参阅我在 上的部分回答,了解使用 may_alias 加载以安全读取缓冲区作为 longs 的另一个示例,这与 glibc 的版本不同,它只是“安全”因为它是用已知的编译器编译的,并且不可能内联到加载或存储具有不同类型的相同内存的调用程序中。 (那个确实需要对齐,所以我省略了 aligned(1) 属性。)