是否可以将字符串强制转换为C中的整数/长表示形式



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

#include <stdio.h>
static char str[16];
int main(void)
{
*(long *)str = 0x006f6c6c6568;
printf("%sn", str);
return 0;
}

我正在运行macOS,它使用小端序,所以0x006f6c6c6568转换为hello。该程序编译时没有错误或警告,运行时会按预期打印出hello。我手工计算了0x006f6c6c6568,但我想知道C是否能帮我做

#include <stdio.h>
static char str[16];
int main(void)
{
// *(long *)str = 0x006f6c6c6568;
*(str+0) = "hello";
printf("%sn", 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));

或者,如果为big-endian目标编译,则为:

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

想法?

是否有一些内置的C函数/方法/预处理器函数/运算符等。它可以将8个字符的字符串转换为长类型的原始十六进制表示

我知道你已经接受了答案,但我认为这个解决方案更容易理解,可能也是你想要的。

只需要将字符串字节复制到64位整数类型中。我将使用uint64_t而不是long,因为在所有平台上都保证是8字节。CCD_ 10通常只有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。整数的对象表示将是那些字符串字节。

字符串总是在最低地址处有第一个char,即char元素的序列,因此字节序不是一个因素,因为char中没有寻址。与依赖于端序的整数不同,哪一端是最低有效字节。

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

CCD_ 26避免了来自对准的可能的未定义行为和来自CCD_。即memcpy(str, &val, sizeof(val))是一种安全的方式,可以在C中表达未对齐的严格混叠安全的8字节加载或存储,就像在x86-64 asm中使用mov一样
(GNU C还允许您使用typedef uint64_t aliasing_u64 __attribute__((aligned(1), may_alias));-您可以将其指向任何位置并安全地读取/写入,就像使用8字节内存一样。)

char*unsigned char*可以在ISO C中别名任何其他类型,因此使用memcpy甚至strncpy来编写其他类型的对象表示是安全的,尤其是那些具有保证格式/布局的类型,如uint64_t(固定宽度,如果存在,则无填充)。


如果要将较短的字符串零填充为整数的完整大小,请使用strncpy。在小端序机器上,它就像宽度为CHAR_BIT * strlen()的整数被零扩展到64位,因为字符串后面多余的零字节进入表示整数最高有效位的字节。

在大端序机器上,值的低位将为零,就好像你左移了"0";窄整数";到更宽整数的顶部。(非零字节的顺序不同)
在混合端序机器(例如PDP-11)上,描述起来不那么简单。

strncpy不适合实际字符串,但正是我们想要的。对于普通的字符串复制来说,这是低效的,因为它总是写入指定的长度(浪费时间,并为短副本触摸长缓冲区中未使用的部分)。它对字符串的安全性不是很有用,因为它不会为大型源字符串的终止零留下空间
但这两件事正是我们想要/需要的:对于长度为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
}

这非常简单,在Godbolt编译器资源管理器上使用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

在ISAs上,未对齐的加载在最坏的情况下只会带来速度损失,例如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有两个原因:第一,这样你就可以在不必手动更改硬编码大小的情况下更改类型。

其次,因为一些晦涩难懂的C实现(如具有字可寻址存储器的现代DSP)没有CHAR_BIT == 8。通常为16或24,具有sizeof(int) == 1,即与char相同。我不确定字节在字符串文字中的具体排列方式,比如每个char单词是否有一个字符,或者是否只有一个8个字母的字符串,包含不到8个chars,但至少在局部变量之外写入时不会有未定义的行为。

示例:带有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这样的编译器警告-Wall的这些有效用例的原因。这和我们的非标准用例,其中我们希望截断更长的字符串文字,只是为了演示它的工作方式。这不是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
|     ~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

大端示例:PowerPC64

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

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

作为C++编译以允许函数返回值作为全局变量的初始值设定项,PowerPC64的clang(trunk)以恒定传播的方式将上述内容编译到这些全局变量的.data中的初始化静态存储中,而不是调用";构造函数";不幸的是,在启动时像GCC一样存储到BSS中。(这很奇怪,因为GCC的初始值设定器函数只是从即时性本身构造值并存储。)

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

tests1()的asm一次只能从16位的立即数构造常量(因为指令只有32位宽,其中一些空间用于操作码和寄存器号)。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

我在C++中的全局范围内为uint64_t foo = tests1()的初始化器使用常量传播(C首先不允许使用非常量初始化器),看看我是否可以让GCC做clang所做的事情。到目前为止没有成功。即使使用constexpr和C++20std::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类型的双关语相关的例子。


感谢@selbie的strncpy想法和代码的起点;出于某种原因,他们将答案改为更复杂,并避免使用strncpy,因此,假设strncpy的库实现使用手工编写的asm,那么当不发生恒定传播时,速度可能会更慢。但无论哪种方式,仍然使用字符串文字进行内联和优化。

他们目前将strnlenmemcpy转换为零初始化的value的答案在正确性方面与此完全等效,但对运行时变量字符串的编译效率较低。

添加#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

最新更新