C++ String
In this guide, we will learn how std::string is laid out in memory and how to read it externally.
In this guide, we will learn how std::string is laid out in memory and how to read it externally.
When reading std::string from an external process on MSVC x64, we need to understand its exact memory layout. The MSVC implementation of std::string is exactly 32 bytes and includes an optimization called Short String Optimization (SSO).
The 32-byte structure is divided into three parts:
union _Bxty — either char _Buf[16] (short string data stored inline) or char* _Ptr (pointer to heap-allocated buffer for long strings)size_t _Mysize — the current length of the stringsize_t _Myres — the capacity of the string. If > 15 (_BUF_SIZE), it is a long string using _Ptr. If <= 15, it is a short string using _Buf directly.When _Myres > 15, the string is stored on the heap and the first 8 bytes of the union contain a pointer to the heap buffer. When _Myres <= 15, the string data fits entirely within the 16-byte _Buf array and no heap allocation occurs.
The following Zig code reads a std::string from external process memory, handling both the short and long string cases.
pub fn readString(memory_accessor: MemoryAccessor, str_address: usize, buffer: []u8) ![]const u8 {
// Read the entire data structure at once:
const StringData = extern struct {
sso: extern union {
short: [16]u8,
ptr: usize,
}, // _Bxty union (short data or heap pointer)
len: usize, // _Mysize
capacity: usize, // _Myres
};
const string_data = try memory_accessor.read(str_address, StringData);
if(buffer.len < string_data.len)
return error.InsufficientBufferSize;
const result = buffer[0..string_data.len];
if(string_data.capacity < 16) {
// Short string: string contains the data directly
@memcpy(result, string_data.sso.short[0..string_data.len]);
return result;
}
// Long string: first 8 bytes are the heap pointer
try memory_accessor.readSlice(string_data.sso.ptr, u8, result);
return result;
}
The StringData extern struct mirrors the 32-byte MSVC std::string layout exactly, so reading it requires just one memory_accessor.read call. readString then inspects capacity (aka _Myres) to decide whether to dereference a heap pointer (long string) or copy inline bytes (short string), returning a directly usable []u8 slice.
Roblox Instance objects store their name as a std::string pointer at the Name offset from the config. Here is how you would read it:
pub fn main() !void {
const process_handle = win32.GetCurrentProcess(); // Replace with OpenProcess
const memory_accessor: MemoryAccessor = .init(process_handle);
const instance_ptr: usize = 0x12345678; // Replace with actual address
var buffer: [4096]u8 = undefined;
const name_address = try memory_accessor.read(instance_ptr + Config.get("Offset/Instance/Name"), usize);
const name = try readString(memory_accessor, name_address, &buffer);
std.debug.print("Instance name: {s}\n", .{name});
}