
Matthew Pickering pushed to branch wip/stable-ipe-info at Glasgow Haskell Compiler / GHC Commits: 5d14950e by Matthew Pickering at 2025-07-01T19:30:09+01:00 ipe: Place strings and metadata into specific .ipe section By placing the .ipe metadata into a specific section it can be stripped from the final binary if desired. ``` objcopy --remove-section .ipe <binary> upx <binary> ``` Towards #21766 - - - - - c4de29a4 by Matthew Pickering at 2025-07-02T12:35:06+01:00 ipe: Place magic word at the start of entries in the .ipe section The magic word "IPE\nIPE\n" is placed at the start of .ipe sections, then if the section is stripped, we can check whether the section starts with the magic word or not to determine whether there is metadata present or not. Towards #21766 - - - - - 59ec75c4 by Matthew Pickering at 2025-07-02T12:35:06+01:00 ipe: Use stable IDs for IPE entries IPEs have historically been indexed and reported by their address. This makes it impossible to compare profiles between runs, since the addresses may change (due to ASLR) and also makes it tricky to separate out the IPE map from the binary. This small patch adds a stable identifier for each IPE entry. The stable identifier is a single 64 bit word. The high-bits are a per-module identifier and the low bits identify which entry in each module. 1. When a node is added into the IPE buffer it is assigned a unique identifier from an incrementing global counter. 2. Each entry already has an index by it's position in the `IpeBufferListNode`. The two are combined together by the `IPE_ENTRY_KEY` macro. Info table profiling uses the stable identifier rather than the address of the info table. The benefits of this change are: * Profiles from different runs can be easily compared * The metadata can be extracted from the binary (via the eventlog for example) and then stripped from the executable. Fixes #21766 - - - - - 11 changed files: - compiler/GHC/Cmm.hs - compiler/GHC/CmmToAsm/PPC/Ppr.hs - compiler/GHC/CmmToAsm/Ppr.hs - compiler/GHC/CmmToLlvm/Data.hs - compiler/GHC/StgToCmm/InfoTableProv.hs - rts/IPE.c - rts/ProfHeap.c - rts/eventlog/EventLog.c - rts/include/rts/IPE.h - testsuite/tests/rts/ipe/ipeMap.c - testsuite/tests/rts/ipe/ipe_lib.c Changes: ===================================== compiler/GHC/Cmm.hs ===================================== @@ -278,6 +278,7 @@ data SectionType | InitArray -- .init_array on ELF, .ctor on Windows | FiniArray -- .fini_array on ELF, .dtor on Windows | CString + | IPE | OtherSection String deriving (Show) @@ -298,6 +299,7 @@ sectionProtection (Section t _) = case t of CString -> ReadOnlySection Data -> ReadWriteSection UninitialisedData -> ReadWriteSection + IPE -> ReadWriteSection (OtherSection _) -> ReadWriteSection {- @@ -557,4 +559,5 @@ pprSectionType s = doubleQuotes $ case s of InitArray -> text "initarray" FiniArray -> text "finiarray" CString -> text "cstring" + IPE -> text "ipe" OtherSection s' -> text s' ===================================== compiler/GHC/CmmToAsm/PPC/Ppr.hs ===================================== @@ -285,6 +285,9 @@ pprAlignForSection platform seg = line $ Data | ppc64 -> text ".align 3" | otherwise -> text ".align 2" + IPE + | ppc64 -> text ".align 3" + | otherwise -> text ".align 2" ReadOnlyData | ppc64 -> text ".align 3" | otherwise -> text ".align 2" ===================================== compiler/GHC/CmmToAsm/Ppr.hs ===================================== @@ -236,6 +236,10 @@ pprGNUSectionHeader config t suffix = | OSMinGW32 <- platformOS platform -> text ".rdata" | otherwise -> text ".rodata.str" + IPE + | OSMinGW32 <- platformOS platform + -> text ".rdata" + | otherwise -> text ".ipe" OtherSection _ -> panic "PprBase.pprGNUSectionHeader: unknown section type" flags = case t of @@ -248,6 +252,10 @@ pprGNUSectionHeader config t suffix = | OSMinGW32 <- platformOS platform -> empty | otherwise -> text ",\"aMS\"," <> sectionType platform "progbits" <> text ",1" + IPE + | OSMinGW32 <- platformOS platform + -> empty + | otherwise -> text ",\"a\"," <> sectionType platform "progbits" _ -> empty {-# SPECIALIZE pprGNUSectionHeader :: NCGConfig -> SectionType -> CLabel -> SDoc #-} {-# SPECIALIZE pprGNUSectionHeader :: NCGConfig -> SectionType -> CLabel -> HLine #-} -- see Note [SPECIALIZE to HDoc] in GHC.Utils.Outputable @@ -262,6 +270,7 @@ pprXcoffSectionHeader t = case t of RelocatableReadOnlyData -> text ".csect .text[PR] # RelocatableReadOnlyData" CString -> text ".csect .text[PR] # CString" UninitialisedData -> text ".csect .data[BS]" + IPE -> text ".csect .text[PR] #IPE" _ -> panic "pprXcoffSectionHeader: unknown section type" {-# SPECIALIZE pprXcoffSectionHeader :: SectionType -> SDoc #-} {-# SPECIALIZE pprXcoffSectionHeader :: SectionType -> HLine #-} -- see Note [SPECIALIZE to HDoc] in GHC.Utils.Outputable @@ -276,6 +285,7 @@ pprDarwinSectionHeader t = case t of InitArray -> text ".section\t__DATA,__mod_init_func,mod_init_funcs" FiniArray -> panic "pprDarwinSectionHeader: fini not supported" CString -> text ".section\t__TEXT,__cstring,cstring_literals" + IPE -> text ".const" OtherSection _ -> panic "pprDarwinSectionHeader: unknown section type" {-# SPECIALIZE pprDarwinSectionHeader :: SectionType -> SDoc #-} {-# SPECIALIZE pprDarwinSectionHeader :: SectionType -> HLine #-} -- see Note [SPECIALIZE to HDoc] in GHC.Utils.Outputable ===================================== compiler/GHC/CmmToLlvm/Data.hs ===================================== @@ -145,7 +145,7 @@ llvmSectionType p t = case t of CString -> case platformOS p of OSMinGW32 -> fsLit ".rdata$str" _ -> fsLit ".rodata.str" - + IPE -> fsLit ".ipe" InitArray -> panic "llvmSectionType: InitArray" FiniArray -> panic "llvmSectionType: FiniArray" OtherSection _ -> panic "llvmSectionType: unknown section type" ===================================== compiler/GHC/StgToCmm/InfoTableProv.hs ===================================== @@ -66,6 +66,28 @@ construction, the 'compressed' field of each IPE buffer list node is examined. If the field indicates that the data has been compressed, the entry data and strings table are decompressed before continuing with the normal IPE map construction. + +Note [IPE Stripping and magic words] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For systems which support ELF executables: + +The metadata part of IPE info is placed into a separate ELF section (.ipe). +This can then be stripped afterwards if you don't require the metadata + +``` +-- Remove the section +objcopy --remove-section .ipe <your-exe> +-- Repack and compress the executable +upx <your-exe> +``` + +The .ipe section starts with a magic 64-bit word "IPE\nIPE\n`, encoded as ascii. + +The RTS checks to see if the .ipe section starts with the magic word. If the +section has been stripped then it won't start with the magic word and the +metadata won't be accessible for the info tables. + -} emitIpeBufferListNode :: @@ -124,11 +146,21 @@ emitIpeBufferListNode this_mod ents dus0 = do ipe_buffer_lbl :: CLabel ipe_buffer_lbl = mkIPELabel this_mod + -- A magic word we can use to see if the IPE information has been stripped + -- or not + -- See Note [IPE Stripping and magic words] + -- "IPE\nIPE\n", null terminated. + ipe_header :: CmmStatic + ipe_header = CmmStaticLit (CmmInt 0x4950450049504500 W64) + ipe_buffer_node :: [CmmStatic] ipe_buffer_node = map CmmStaticLit [ -- 'next' field zeroCLit platform + -- 'node_id' field + , zeroCLit platform + -- 'compressed' field , int do_compress @@ -164,13 +196,13 @@ emitIpeBufferListNode this_mod ents dus0 = do -- Emit the strings table emitDecl $ CmmData - (Section Data strings_lbl) - (CmmStaticsRaw strings_lbl strings) + (Section IPE strings_lbl) + (CmmStaticsRaw strings_lbl (ipe_header : strings)) -- Emit the list of IPE buffer entries emitDecl $ CmmData - (Section Data entries_lbl) - (CmmStaticsRaw entries_lbl entries) + (Section IPE entries_lbl) + (CmmStaticsRaw entries_lbl (ipe_header : entries)) -- Emit the IPE buffer list node emitDecl $ CmmData ===================================== rts/IPE.c ===================================== @@ -62,6 +62,22 @@ entry's containing IpeBufferListNode and its index in that node. When the user looks up an IPE entry, we convert it to the user-facing InfoProvEnt representation. +Note [Stable identifiers for IPE entries] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each IPE entry is given a stable identifier which remains the same across +different runs of the executable (unlike the address of the info table). + +The identifier is a 64-bit word which consists of two parts. + +* The high 32-bits are a per-node identifier. +* The low 32-bits are the index of the entry in the node. + +When a node is queued in the pending list by `registerInfoProvList` it is +given a unique identifier from an incrementing global variable. + +The unique key can be computed by using the `IPE_ENTRY_KEY` macro. + */ typedef struct { @@ -69,6 +85,13 @@ typedef struct { uint32_t idx; } IpeMapEntry; +// See Note [Stable identifiers for IPE entries] +#define IPE_ENTRY_KEY(entry) \ + MAKE_IPE_KEY((entry).node->node_id, (entry).idx) + +#define MAKE_IPE_KEY(module_id, idx) \ + ((((uint64_t)(module_id)) << 32) | ((uint64_t)(idx))) + #if defined(THREADED_RTS) static Mutex ipeMapLock; #endif @@ -78,9 +101,22 @@ static HashTable *ipeMap = NULL; // Accessed atomically static IpeBufferListNode *ipeBufferList = NULL; +// A global counter which is used to give an IPE entry a unique value across runs. +static uint32_t next_module_id = 1; // Start at 1 to reserve 0 as "invalid" + static void decompressIPEBufferListNodeIfCompressed(IpeBufferListNode*); static void updateIpeMap(void); +// Check whether the IpeBufferListNode has the relevant magic words. +// See Note [IPE Stripping and magic words] +static inline bool ipe_node_valid(const IpeBufferListNode *node) { + return node && + node->entries_block && + node->string_table_block && + node->entries_block->magic == IPE_MAGIC_WORD && + node->string_table_block->magic == IPE_MAGIC_WORD; +} + #if defined(THREADED_RTS) void initIpe(void) { initMutex(&ipeMapLock); } @@ -99,11 +135,12 @@ static InfoProvEnt ipeBufferEntryToIpe(const IpeBufferListNode *node, uint32_t i { CHECK(idx < node->count); CHECK(!node->compressed); - const char *strings = node->string_table; - const IpeBufferEntry *ent = &node->entries[idx]; + const char *strings = node->string_table_block->string_table; + const IpeBufferEntry *ent = &node->entries_block->entries[idx]; return (InfoProvEnt) { .info = node->tables[idx], .prov = { + .info_prov_id = MAKE_IPE_KEY(node->node_id, idx), .table_name = &strings[ent->table_name], .closure_desc = ent->closure_desc, .ty_desc = &strings[ent->ty_desc], @@ -121,19 +158,23 @@ static InfoProvEnt ipeBufferEntryToIpe(const IpeBufferListNode *node, uint32_t i static void traceIPEFromHashTable(void *data STG_UNUSED, StgWord key STG_UNUSED, const void *value) { const IpeMapEntry *map_ent = (const IpeMapEntry *)value; - const InfoProvEnt ipe = ipeBufferEntryToIpe(map_ent->node, map_ent->idx); - traceIPE(&ipe); + if (ipe_node_valid(map_ent->node)){ + const InfoProvEnt ipe = ipeBufferEntryToIpe(map_ent->node, map_ent->idx); + traceIPE(&ipe); + } } void dumpIPEToEventLog(void) { // Dump pending entries IpeBufferListNode *node = RELAXED_LOAD(&ipeBufferList); while (node != NULL) { - decompressIPEBufferListNodeIfCompressed(node); + if (ipe_node_valid(node)){ + decompressIPEBufferListNodeIfCompressed(node); - for (uint32_t i = 0; i < node->count; i++) { - const InfoProvEnt ent = ipeBufferEntryToIpe(node, i); - traceIPE(&ent); + for (uint32_t i = 0; i < node->count; i++) { + const InfoProvEnt ent = ipeBufferEntryToIpe(node, i); + traceIPE(&ent); + } } node = node->next; } @@ -170,6 +211,8 @@ void registerInfoProvList(IpeBufferListNode *node) { while (true) { IpeBufferListNode *old = RELAXED_LOAD(&ipeBufferList); node->next = old; + uint32_t module_id = next_module_id++; + node->node_id = module_id; if (cas_ptr((volatile void **) &ipeBufferList, old, node) == (void *) old) { return; } @@ -183,7 +226,7 @@ void formatClosureDescIpe(const InfoProvEnt *ipe_buf, char *str_buf) { bool lookupIPE(const StgInfoTable *info, InfoProvEnt *out) { updateIpeMap(); IpeMapEntry *map_ent = (IpeMapEntry *) lookupHashTable(ipeMap, (StgWord)info); - if (map_ent) { + if (map_ent && ipe_node_valid(map_ent->node)) { *out = ipeBufferEntryToIpe(map_ent->node, map_ent->idx); return true; } else { @@ -191,6 +234,18 @@ bool lookupIPE(const StgInfoTable *info, InfoProvEnt *out) { } } +// Returns 0 when the info table is not present in the info table map. +// See Note [Stable identifiers for IPE entries] +uint64_t lookupIPEId(const StgInfoTable *info) { + updateIpeMap(); + IpeMapEntry *map_ent = (IpeMapEntry *) lookupHashTable(ipeMap, (StgWord)(info)); + if (map_ent){ + return IPE_ENTRY_KEY(*map_ent); + } else { + return 0; + } +} + void updateIpeMap(void) { // Check if there's any work at all. If not so, we can circumvent locking, // which decreases performance. ===================================== rts/ProfHeap.c ===================================== @@ -230,9 +230,15 @@ closureIdentity( const StgClosure *p ) return closure_type_names[info->type]; } } - case HEAP_BY_INFO_TABLE: { - return get_itbl(p); + case HEAP_BY_INFO_TABLE: + { + uint64_t table_id = lookupIPEId(p->header.info); + if (table_id) { + return (void *) table_id; + } else { + return (void *) 0xffffffff; } + } default: barf("closureIdentity"); ===================================== rts/eventlog/EventLog.c ===================================== @@ -1472,7 +1472,7 @@ void postIPE(const InfoProvEnt *ipe) CHECK(!ensureRoomForVariableEvent(&eventBuf, len)); postEventHeader(&eventBuf, EVENT_IPE); postPayloadSize(&eventBuf, len); - postWord64(&eventBuf, (StgWord) INFO_PTR_TO_STRUCT(ipe->info)); + postWord64(&eventBuf, (StgWord) (ipe->prov.info_prov_id)); postStringLen(&eventBuf, ipe->prov.table_name, table_name_len); postStringLen(&eventBuf, closure_desc_buf, closure_desc_len); postStringLen(&eventBuf, ipe->prov.ty_desc, ty_desc_len); ===================================== rts/include/rts/IPE.h ===================================== @@ -14,6 +14,7 @@ #pragma once typedef struct InfoProv_ { + uint64_t info_prov_id; const char *table_name; uint32_t closure_desc; // closure type const char *ty_desc; @@ -63,9 +64,26 @@ typedef struct { GHC_STATIC_ASSERT(sizeof(IpeBufferEntry) % (WORD_SIZE_IN_BITS / 8) == 0, "sizeof(IpeBufferEntry) must be a multiple of the word size"); +// The magic word is IPE\nIPE\n, which occupies the full 64 bit width of a word. +// See Note [IPE Stripping and magic words] +#define IPE_MAGIC_WORD 0x4950450049504500UL + +typedef struct { + StgWord magic; // Must be IPE_MAGIC_WORD + IpeBufferEntry entries[]; // Flexible array member +} IpeBufferEntryBlock; + +typedef struct { + StgWord magic; // Must be IPE_MAGIC_WORD + char string_table[]; // Flexible array member for string table +} IpeStringTableBlock; + typedef struct IpeBufferListNode_ { struct IpeBufferListNode_ *next; + // This field is filled in when the node is registered. + uint32_t node_id; + // Everything below is read-only and generated by the codegen // This flag should be treated as a boolean @@ -76,10 +94,10 @@ typedef struct IpeBufferListNode_ { // When TNTC is enabled, these will point to the entry code // not the info table itself. const StgInfoTable **tables; - IpeBufferEntry *entries; + IpeBufferEntryBlock *entries_block; StgWord entries_size; // decompressed size - const char *string_table; + const IpeStringTableBlock *string_table_block; StgWord string_table_size; // decompressed size // Shared by all entries @@ -98,6 +116,8 @@ void formatClosureDescIpe(const InfoProvEnt *ipe_buf, char *str_buf); // Returns true on success, initializes `out`. bool lookupIPE(const StgInfoTable *info, InfoProvEnt *out); +uint64_t lookupIPEId(const StgInfoTable *info); + #if defined(DEBUG) void printIPE(const StgInfoTable *info); #endif ===================================== testsuite/tests/rts/ipe/ipeMap.c ===================================== @@ -48,7 +48,8 @@ HaskellObj shouldFindOneIfItHasBeenRegistered(Capability *cap) { // Allocate buffers for IPE buffer list node IpeBufferListNode *node = malloc(sizeof(IpeBufferListNode)); node->tables = malloc(sizeof(StgInfoTable *)); - node->entries = malloc(sizeof(IpeBufferEntry)); + node->entries_block = malloc(sizeof(StgWord64) + sizeof(IpeBufferEntry)); + node->entries_block->magic = IPE_MAGIC_WORD; StringTable st; init_string_table(&st); @@ -61,9 +62,13 @@ HaskellObj shouldFindOneIfItHasBeenRegistered(Capability *cap) { node->compressed = 0; node->count = 1; node->tables[0] = get_itbl(fortyTwo); - node->entries[0] = makeAnyProvEntry(cap, &st, 42); + node->entries_block->entries[0] = makeAnyProvEntry(cap, &st, 42); node->entries_size = sizeof(IpeBufferEntry); - node->string_table = st.buffer; + + IpeStringTableBlock *string_table_block = malloc(sizeof(StgWord64) + st.size); + string_table_block->magic = IPE_MAGIC_WORD; + memcpy(string_table_block->string_table, st.buffer, st.size); + node->string_table_block = string_table_block; node->string_table_size = st.size; registerInfoProvList(node); @@ -90,7 +95,8 @@ void shouldFindTwoIfTwoHaveBeenRegistered(Capability *cap, // Allocate buffers for IPE buffer list node IpeBufferListNode *node = malloc(sizeof(IpeBufferListNode)); node->tables = malloc(sizeof(StgInfoTable *)); - node->entries = malloc(sizeof(IpeBufferEntry)); + node->entries_block = malloc(sizeof(StgWord64) + sizeof(IpeBufferEntry)); + node->entries_block->magic = IPE_MAGIC_WORD; StringTable st; init_string_table(&st); @@ -103,9 +109,12 @@ void shouldFindTwoIfTwoHaveBeenRegistered(Capability *cap, node->compressed = 0; node->count = 1; node->tables[0] = get_itbl(twentyThree); - node->entries[0] = makeAnyProvEntry(cap, &st, 23); + node->entries_block->entries[0] = makeAnyProvEntry(cap, &st, 23); node->entries_size = sizeof(IpeBufferEntry); - node->string_table = st.buffer; + IpeStringTableBlock *string_table_block = malloc(sizeof(StgWord64) + st.size); + string_table_block->magic = IPE_MAGIC_WORD; + memcpy(string_table_block->string_table, st.buffer, st.size); + node->string_table_block = string_table_block; node->string_table_size = st.size; registerInfoProvList(node); @@ -121,7 +130,8 @@ void shouldFindTwoFromTheSameList(Capability *cap) { // Allocate buffers for IPE buffer list node IpeBufferListNode *node = malloc(sizeof(IpeBufferListNode)); node->tables = malloc(sizeof(StgInfoTable *) * 2); - node->entries = malloc(sizeof(IpeBufferEntry) * 2); + node->entries_block = malloc(sizeof(StgWord64) + sizeof(IpeBufferEntry) * 2); + node->entries_block->magic = IPE_MAGIC_WORD; StringTable st; init_string_table(&st); @@ -133,10 +143,13 @@ void shouldFindTwoFromTheSameList(Capability *cap) { node->count = 2; node->tables[0] = get_itbl(one); node->tables[1] = get_itbl(two); - node->entries[0] = makeAnyProvEntry(cap, &st, 1); - node->entries[1] = makeAnyProvEntry(cap, &st, 2); + node->entries_block->entries[0] = makeAnyProvEntry(cap, &st, 1); + node->entries_block->entries[1] = makeAnyProvEntry(cap, &st, 2); node->entries_size = sizeof(IpeBufferEntry) * 2; - node->string_table = st.buffer; + IpeStringTableBlock *string_table_block = malloc(sizeof(StgWord64) + st.size); + string_table_block->magic = IPE_MAGIC_WORD; + memcpy(string_table_block->string_table, st.buffer, st.size); + node->string_table_block = string_table_block; node->string_table_size = st.size; registerInfoProvList(node); @@ -152,7 +165,11 @@ void shouldDealWithAnEmptyList(Capability *cap, HaskellObj fortyTwo) { IpeBufferListNode *node = malloc(sizeof(IpeBufferListNode)); node->count = 0; node->next = NULL; - node->string_table = ""; + IpeStringTableBlock *string_table_block = malloc(sizeof(StgWord64)); + string_table_block->magic = IPE_MAGIC_WORD; + + node->entries_block = malloc(sizeof(StgWord64)); + node->entries_block->magic = IPE_MAGIC_WORD; registerInfoProvList(node); ===================================== testsuite/tests/rts/ipe/ipe_lib.c ===================================== @@ -64,7 +64,8 @@ IpeBufferListNode *makeAnyProvEntries(Capability *cap, int start, int end) { // Allocate buffers for IpeBufferListNode IpeBufferListNode *node = malloc(sizeof(IpeBufferListNode)); node->tables = malloc(sizeof(StgInfoTable *) * n); - node->entries = malloc(sizeof(IpeBufferEntry) * n); + node->entries_block = malloc(sizeof(StgWord64) + sizeof(IpeBufferEntry) * n); + node->entries_block->magic = IPE_MAGIC_WORD; StringTable st; init_string_table(&st); @@ -83,14 +84,19 @@ IpeBufferListNode *makeAnyProvEntries(Capability *cap, int start, int end) { for (int i=start; i < end; i++) { HaskellObj closure = rts_mkInt(cap, 42); node->tables[i] = get_itbl(closure); - node->entries[i] = makeAnyProvEntry(cap, &st, i); + node->entries_block->entries[i] = makeAnyProvEntry(cap, &st, i); } // Set the rest of the fields node->next = NULL; node->compressed = 0; node->count = n; - node->string_table = st.buffer; + + IpeStringTableBlock *string_table_block = + malloc(sizeof(StgWord64) + st.size); + string_table_block->magic = IPE_MAGIC_WORD; + memcpy(string_table_block->string_table, st.buffer, st.size); + node->string_table_block = string_table_block; return node; } View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/0317f0612514d1c4cf9dfcfe526380c... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/0317f0612514d1c4cf9dfcfe526380c... You're receiving this email because of your account on gitlab.haskell.org.