在反編譯各種程式(我沒有源代碼)后,我發現了一些有趣的代碼序列。程式str在 DATA 部分中定義了一個 c 字串 ( )。在 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;
}
我正在運行 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));
想法?
uj5u.com熱心網友回復:
是否有一些內置的 C 函式/方法/前處理器函式/運算子/等。可以將 8 個字符的字串轉換為其原始十六進制表示的 long 型別
我看到你已經接受了一個答案,但我認為這個解決方案更容易理解并且可能是你想要的。
只需將字串位元組復制為 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
uj5u.com熱心網友回復:
TL:DR: 你想strncpy進入uint64_t. 這個答案很長,試圖解釋概念以及如何從 C 與 asm 的角度以及整個整數與單個chars / 位元組的角度來考慮記憶體。(即如果很明顯 strlen/memcpy 或 strncpy 會做你想做的事,直接跳到代碼。)
如果您想將 8 個位元組的字串資料準確地復制到一個整數中,請使用memcpy. 整數的物件表示將是那些字串位元組。
字串總是在最低地址處具有第一個char,即一系列char元素,因此位元組序不是一個因素,因為在 a 中沒有尋址char。與整數不同,它取決于位元組序,哪一端是最低有效位元組。
將此整數存盤到記憶體中將具有與原始字串相同的位元組順序,就像您memcpy對char tmp[8]陣列而不是uint64_t tmp. (C 本身沒有任何記憶體與暫存器的概念;每個物件都有一個地址,除非通過 as-if 規則進行優化允許,但是分配給某些陣列元素可以讓真正的編譯器使用存盤指令,而不是僅僅放置暫存器中的常量。因此您可以使用除錯器查看這些位元組,并查看它們的順序是否正確。或者傳遞一個指向fwriteorputs或其他的指標。)
memcpy避免來自對齊和嚴格混疊違規的可能未定義行為*(uint64_t*)str = val;。iememcpy(str, &val, sizeof(val))是一種在 C 中表達未對齊的嚴格別名安全的 8 位元組加載或存盤的安全方式,就像您可以mov在 x86-64 asm 中輕松完成一樣。
(GNU C 也允許你 typedef uint64_t aliasing_u64 __attribute__((aligned(1), may_alias));——你可以指向任何東西并安全地讀/寫它,就像使用 8 位元組的 memcpy 一樣。)
char*并且unsigned char*可以在 ISO C 中為任何其他型別設定別名,因此使用 memcpy 甚至strncpy撰寫其他型別的物件表示是安全的,尤其是那些具有保證格式/布局的物件表示,例如uint64_t(固定寬度,無填充,如果它存在的話) )。
如果您想要較短的字串以零填充到整數的完整大小,請使用strncpy. 在 little-endian 機器上,它就像一個寬度整數CHAR_BIT * strlen()被零擴展為 64 位,因為字串后面的額外零位元組進入表示整數最高有效位的位元組。
在大端機器上,該值的低位將為零,就好像您將“窄整數”左移到更寬整數的頂部一樣。(并且非零位元組的順序不同。彼此)。
在混合端機器(例如 PDP-11)上,描述起來沒那么簡單。
strncpy對實際的字串不好,但正是我們想要的。對于普通的字串復制來說效率很低,因為它總是寫出指定的長度(浪費時間并觸摸長緩沖區的其他未使用部分以進行短副本)。而且它對于字串的安全性不是很有用,因為它不會為帶有大源字串的終止零留出空間。
但是這兩件事正是我們在這里想要/需要的:它的行為類似于memcpy(val, str, 8)長度為 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
}
這編譯非常簡單,使用 GCC 或Godbolt 編譯器資源管理器上的一個 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出于兩個原因:首先,您可以更改型別而無需手動更改硬編碼的大小。
其次,因為一些不起眼的 C 實作(例如具有字可尋址存盤器的現代 DSP)沒有CHAR_BIT == 8. 通常是 16 或 24,與sizeof(int) == 1ie 相同char。我不確定位元組在字串文字中的確切排列方式,例如每個char單詞是否有一個字符,或者您是否只有一個少于 8 的 8 個字母的字串chars,但至少您不會t 在區域變數外部寫入時具有未定義的行為。
示例:短字串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 并不想 inline 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 \0\0\0\0\0\0"
asm fortests1()一次只能從立即數 16 位構造一個常量(因為一條指令只有 32 位寬,并且操作碼和暫存器號需要一些空間)。 神螺栓
# 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
我玩了一下,讓常量傳播為uint64_t foo = tests1()C 中全域范圍的初始化程式作業(C 首先不允許非常量初始化程式),看看我是否可以讓 GCC 做 clang 所做的事情。至今沒有成功。即使使用constexprC 20 std::bit_cast<uint64_t>(struct_of_char_array),我也無法讓 g 或 clang 接受在語言實際需要 auint64_t foo[stringbytes2("h")]的背景關系中使用整數值,而不僅僅是一種優化。 神箭。constexpr
IIRC std::bit_cast 應該能夠從字串文字中制造一個 constexpr 整數,但我可能忘記了一些技巧;我還沒有搜索現有的 SO 答案。我似乎記得看到一個bit_cast與某種 constexpr 型別雙關語相關的地方。
感謝@selbie 的strncpy想法和代碼的起點;出于某種原因,他們將答案更改為更復雜并避免strncpy,因此如果沒有發生常量傳播,假設strncpy使用手寫 asm 的良好庫實作可能會更慢。但無論哪種方式仍然使用字串文字進行行內和優化。
就正確性而言,他們當前對零初始化的答案與strnlen此完全等效,但對于運行時變數字串的編譯效率較低。memcpyvalue
uj5u.com熱心網友回復:
添加#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
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/463856.html
