是否可以将字符串转换为 C 中的 integer/long 表示形式?

Is it possible to cast a string into its integer/long representation in C?

在反编译各种程序(我没有源程序)后,我发现了一些有趣的代码序列。程序在 DATA 部分中定义了一个 c 字符串 (str)。在 TEXT 部分的某些函数中,通过将十六进制数移动到字符串中的某个位置来设置该字符串的一部分(简化的 Intel 程序集 MOV str,0x006f6c6c6568)。这是 C:

中的一个片段
#include <stdio.h>

static char str[16];

int main(void)
{
    *(long *)str = 0x006f6c6c6568;
    printf("%s\n", str);
    return 0;
}

我是运行ning macOS,它使用little endian,所以0x006f6c6c6568翻译成hello。该程序编译时没有错误或警告,当 运行 时,按预期打印出 hello。我手工计算了 0x006f6c6c6568,但我想知道 C 是否可以为我完成。我的意思是这样的:

#include <stdio.h>

static char str[16];

int main(void)
{
    // *(long *)str = 0x006f6c6c6568;
    *(str+0) = "hello";
    printf("%s\n", str);
    return 0;
}

现在,我不想将 "hello" 视为字符串文字,对于 little-endian 可能会这样处理:

    *(long *)str = (long)(((long)'h') |
                          ((long)'e' << 8) |
                          ((long)'l' << 16) |
                          ((long)'l' << 24) |
                          ((long)'o' << 32) |
                          ((long)0 << 40));

或者,如果针对大端目标编译,则:

    *(long *)str = (long)(((long) 0  << 16) |
                          ((long)'o' << 24) |
                          ((long)'l' << 32) |
                          ((long)'l' << 40) |
                          ((long)'e' << 48) |
                          ((long)'h' << 56));

想法?

加上#if __BYTE_ORDER__判断,像这样:

#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    *(long *)str = (long)(((long)'h') |
                          ((long)'e' << 8) |
                          ((long)'l' << 16) |
                          ((long)'l' << 24) |
                          ((long)'o' << 32) |
                          ((long)0 << 40));
#else
    *(long *)str = (long)((0 |
                           ((long)'o' << 8) |
                           ((long)'l' << 16) |
                           ((long)'l' << 24) |
                           ((long)'e' << 32) |
                           ((long)'h' << 40));
#endif

is there some built-in C function/method/preprocessor function/operator/etc. that can convert an 8 character string into its raw hexadecimal representation of long type

我看到你已经接受了一个答案,但我认为这个解决方案更容易理解并且可能是你想要的。

只需将字符串字节复制为 64 位整数类型即可。我将使用 uint64_t 而不是 long,因为它在所有平台上都保证为 8 个字节。 long 通常只有 4 个字节。

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

uint64_t packString(const char* str) {
    uint64_t value = 0;
    size_t copy = str ? strnlen(str, sizeof(value)) : 0; // copy over at most 8 bytes

    memcpy(&value, str, copy);
    return value;
}

示例:

int main() {
    printf("0x%" PRIx64 "\n", packString("hello"));
    return 0;
}

然后构建 运行:

$:~/code/packString$ g++ main.cpp -o main

$:~/code/packString$ ./main

0x6f6c6c6568

TL:DR: 你想 strncpy 变成 uint64_t。这个答案很长,试图解释这些概念以及如何从 C 与 asm 的角度,以及整个整数与单个 chars / 字节来思考内存。 (即,如果很明显 strlen/memcpy 或 strncpy 会执行您想要的操作,请跳至代码。)


如果要将 8 个字节的字符串数据精确复制为一个整数,请使用 memcpy。整数的 object-representation 将是那些字符串字节。

字符串总是在最低地址处有第一个 char,即 char 元素的序列,因此字节顺序不是一个因素,因为 char 中没有寻址。与 endian-dependent 的整数不同,它的末端是 least-significant 字节。

将此整数存储到内存中将具有与原始字符串相同的字节顺序,就像您对 char tmp[8] 数组而不是 uint64_t tmp 执行 memcpy 一样。 (C 本身没有任何内存与寄存器的概念;每个对象都有一个地址,除非通过 as-if 规则进行优化,但分配给某些数组元素可以让真正的编译器使用存储指令而不是只需将常量放入寄存器中。这样您就可以使用调试器查看这些字节并查看它们的顺序是否正确。或者将指针传递给 fwriteputs 或其他任何内容。)

memcpy 避免了来自对齐的可能的未定义行为和来自 *(uint64_t*)str = val; 的 strict-aliasing 违规。即 memcpy(str, &val, sizeof(val)) 是在 C 中表达未对齐的 strict-aliasing 安全 8 字节加载或存储的安全方式,就像您可以在 x86-64 asm 中使用 mov 轻松做到的那样。
typedef uint64_t aliasing_u64 __attribute__((aligned(1), may_alias)); - 你可以指向任何东西并 read/write 安全地通过它,就像使用 8 字节的 memcpy 一样。)

char*unsigned char* 可以为 ISO C 中的任何其他类型起别名,因此使用 memcpy 甚至 strncpy 编写其他类型的 object-representation 是安全的,特别是那些具有保证格式/布局的格式/布局,例如 uint64_t(固定宽度,无填充,如果存在的话)。


如果您想要更短的字符串 zero-pad 达到整数的完整大小,请使用 strncpy。在 little-endian 机器上,它就像一个宽度 CHAR_BIT * strlen() 的整数 zero-extended 到 64 位,因为字符串后的额外零字节进入表示 most-significant 位的字节整数。

在 big-endian 机器上,值的低位将为零,就好像您 left-shifted 那个“窄整数”到更宽整数的顶部一样。 (并且 non-zero 字节的顺序不同。彼此)。
在 mixed-endian 机器上(例如 PDP-11),描述起来就不那么简单了。

strncpy 对实际字符串不利,但正是我们在这里想要的。它对于普通 string-copying 来说效率低下,因为它 总是 写出指定的长度(浪费时间并触及长缓冲区的其他未使用部分以获取短副本)。而且它对于字符串的安全性不是很有用,因为它没有为大源字符串的终止零留出空间。
但是这两件事正是我们在这里 want/need 所做的:对于长度为 8 或更长的字符串,它的行为类似于 memcpy(val, str, 8),但对于较短的字符串,它不会在整数的高位字节中留下垃圾。

示例:字符串的前 8 个字节

#include <string.h>
#include <stdint.h>

uint64_t load8(const char* str)
{
    uint64_t value;
    memcpy(&value, str, sizeof(value));     // load exactly 8 bytes
    return value;
}

uint64_t test2(){
    return load8("hello world!");  // constant-propagation through it
}

这编译非常简单,在 the Godbolt compiler explorer.

上使用 GCC 或 clang 编译为一条 x86-64 8 字节 mov 指令
load8:
        mov     rax, QWORD PTR [rdi]
        ret

test2:
        movabs  rax, 8031924123371070824  # 0x6F77206F6C6C6568 
          # little-endian "hello wo", note the 0x20 ' ' byte near the top of the value
        ret

在 ISA 上,未对齐的加载最坏的情况下会导致速度损失,例如x86-64 和 PowerPC64,memcpy 可靠地内联。但是在 MIPS64 上你会得到一个函数调用。

# PowerPC64 clang(trunk) -O3
load8:
        ld 3, 0(3)            # r3 = *r3   first arg and return-value register
        blr

顺便说一句,我使用 sizeof(value) 而不是 8 有两个原因:首先,您可以更改类型而无需手动更改 hard-coded 大小。

其次,因为一些晦涩的 C 实现(例如具有 word-addressable 内存的现代 DSP)没有 CHAR_BIT == 8。通常为 16 或 24,sizeof(int) == 1 即与 char 相同。我不确定字节在字符串文字中的确切排列方式,例如每个 char 单词是否有一个字符,或者是否只有少于 8 个 [=] 的 8 个字母的字符串51=],但至少你不会因为在局部变量之外写入而产生未定义的行为。

示例:带有 strncpy

的短字符串
// Take the first 8 bytes of the string, zero-padding if shorter
// (on a big-endian machine, that left-shifts the value, rather than zero-extending)
uint64_t stringbytes(const char* str)
{
    // if (!str)  return 0;   // optional NULL-pointer check
    uint64_t value;           // strncpy always writes the full size (with zero padding if needed)
    strncpy((char*)&value, str, sizeof(value)); // load up to 8 bytes, zero-extending for short strings
    return value;
}

uint64_t tests1(){
    return stringbytes("hello world!");
}
uint64_t tests2(){
    return stringbytes("hi");
}
tests1():
        movabs  rax, 8031924123371070824     # same as with memcpy
        ret
tests2():
        mov     eax, 26984        # 0x6968 = little-endian "hi"
        ret

strncpy 错误功能(这使得它不符合人们希望它的设计目的,strcpy 被截断到一个限制)是为什么像 GCC 这样的编译器警告这些有效的 use-cases 与 -Wall。那和我们的 non-standard use-case,我们 想要 截断更长的字符串文字只是为了演示它是如何工作的。这不是 strncpy 的错,但关于传递与目标实际大小相同的长度限制的警告是。

n function 'constexpr uint64_t stringbytes2(const char*)',
    inlined from 'constexpr uint64_t tests1()' at <source>:26:24:
<source>:20:12: warning: 'char* strncpy(char*, const char*, size_t)' output truncated copying 8 bytes from a string of length 12 [-Wstringop-truncation]
   20 |     strncpy(u.c, str, 8);
      |     ~~~~~~~^~~~~~~~~~~~~
<source>: In function 'uint64_t stringbytes(const char*)':
<source>:10:12: warning: 'char* strncpy(char*, const char*, size_t)' specified bound 8 equals destination size [-Wstringop-truncation]
   10 |     strncpy((char*)&value, str, sizeof(value)); // load up to 8 bytes, zero-extending for short strings
      |     ~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Big-endian 示例:PowerPC64

奇怪的是,MIPS64 的 GCC 不想内联 strnlen,而 PowerPC 无论如何都可以更有效地构造大于 32 位的常量。 (更少的移位指令,因为 oris 可以 OR 到位 [31:16],即 OR 一个移位的立即数。)

uint64_t foo = tests1();
uint64_t bar = tests2();

编译为 C++ 以允许函数 return vaues 作为全局变量的初始值设定项,PowerPC64 的 clang(trunk)将上面的 constant-propagation 编译到这些全局变量的 .data 中的初始化静态存储中,而不是在启动时调用“构造函数”来存储到不幸的是,BSS 就像 GCC 一样。 (这很奇怪,因为 GCC 的初始化函数只是从立即数本身构造值并存储。)

foo:
        .quad   7522537965568948079             # 0x68656c6c6f20776f
                                      # big-endian "h e l l o   w o"

bar:
        .quad   7523544652499124224             # 0x6869000000000000
                                      # big-endian "h i [=17=][=17=][=17=][=17=][=17=][=17=]"

tests1() 的 asm 一次只能从 16 位立即数构造一个常量(因为一条指令只有 32 位宽,其中一些 space 需要用于操作码和寄存器数)。 Godbolt

# GCC11 for PowerPC64 (big-endian mode, not power64le)  -O3 -mregnames 
tests2:
        lis %r3,0x6869    # Load-Immediate Shifted, i.e. big-endian "hi"<<16
        sldi %r3,%r3,32   # Shift Left Doubleword Immediate  r3<<=32 to put it all the way to the top of the 64-bit register
          # the return-value register holds 0x6869000000000000
        blr               # return

tests1():
        lis %r3,0x6865        # big-endian "he"<<16
        ori %r3,%r3,0x6c6c    # OR Immediate producing "hell"
        sldi %r3,%r3,32       # r3 <<= 32
        oris %r3,%r3,0x6f20   # r3 |=  "o " << 16
        ori %r3,%r3,0x776f    # r3 |=  "wo"
          # the return-value register holds 0x68656c6c6f20776f
        blr

我试着让 constant-propagation 在 C++ 的全局范围内为 uint64_t foo = tests1() 的初始值设定项工作(C 首先不允许 non-const 初始值设定项) 看看我是否能让 GCC 做 clang 做的事情。到目前为止没有成功。即使使用 constexpr 和 C++20 std::bit_cast<uint64_t>(struct_of_char_array) 我也无法让 g++ 或 clang++ 接受 uint64_t foo[stringbytes2("h")] 语言 实际上需要 constexpr,而不仅仅是优化。 Godbolt.

IIRC std::bit_cast 应该能够从字符串文字中生成一个 constexpr 整数,但我可能忘记了一些技巧;我还没有搜索现有的 SO 答案。我似乎记得看到一个 bit_cast 与某种 constexpr type-punning.

相关的地方

感谢@selbie 的 strncpy 想法 和代码的起点;出于某种原因,他们将答案更改为更复杂并避免 strncpy,因此当 constant-propagation 没有发生时,它可能会更慢,假设 strncpy 的良好库实现使用 hand-written汇编。但是无论哪种方式仍然内联和优化字符串文字。

他们当前将 strnlenmemcpy 转换为 zero-initialized value 的答案在正确性方面与此完全相同,但编译效率较低 runtime-variable 字符串.