Dynamic linking without libc adventures

Just wanted to let you know that I recently found the time to work on this bad idea, and I made some progress.

I can now do this :

// main.zig

const std = @import("std");

const dynamic_library_loader = @import("dynamic_library_loader.zig");

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{ .stack_trace_frames = 10 }) = .init;
    const allocator = gpa.allocator();
    defer if (gpa.deinit() != .ok) @panic("Memory check failed");

    try dynamic_library_loader.init(.{ .debug = false });
    defer dynamic_library_loader.deinit(allocator);

    const lib = try dynamic_library_loader.load(allocator, "libc.so.6");

    const printf_sym = try lib.getSymbol("printf");
    const printf_addr = printf_sym.addr;
    const printf: *const fn ([*:0]const u8, ...) callconv(.c) c_int = @ptrFromInt(printf_addr);

    _ = printf("Hello, World!\n");
}
$ zig run src/main.zig
Hello, World!

The famous libc printf from a non libc linked static executable :slight_smile: It should be noted that no tricks are used, like the ones in detour and similar projects. This is a real dynamic loader (with lots of TODOs).

This is currently limited to linux x86-64, and it has only been tested on my machine.

I can even do this, thanks to the customizable SelfInfo system:

// main.zig

const std = @import("std");

const dynamic_library_loader = @import("dynamic_library_loader.zig");

pub const debug = struct {
    pub const SelfInfo = dynamic_library_loader.CustomSelfInfo;
};

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{ .stack_trace_frames = 10 }) = .init;
    const allocator = gpa.allocator();
    defer if (gpa.deinit() != .ok) @panic("Memory check failed");

    try dynamic_library_loader.init(.{ .debug = false });
    defer dynamic_library_loader.deinit(allocator);

    const lib = try dynamic_library_loader.load(allocator, "libc.so.6");

    const sprintf_sym = try lib.getSymbol("sprintf");
    const sprintf_addr = sprintf_sym.addr;
    const sprintf: *const fn ([*c]u8, [*c]const u8, ...) callconv(.c) c_int = @ptrFromInt(sprintf_addr);

    var buf: [128:0]u8 = undefined;

    // trigger a segfault to test stack traces
    _ = sprintf(&buf, @ptrFromInt(0x8));
}
$ zig run src/main.zig
Segmentation fault at address 0x8
../sysdeps/x86_64/multiarch/strchr-sse2.S:41:0: 0x7fb48767cf23 in __strchrnul_sse2 (../sysdeps/x86_64/multiarch/strchr-sse2.S)
./stdio-common/printf-parse.h:82:34: 0x7fb48762c669 in __find_specmb (vfprintf-internal.c)
./libio/iovsprintf.c:62:3: 0x7fb48764b208 in __vsprintf_internal (iovsprintf.c)
./stdio-common/sprintf.c:30:10: 0x7fb487629700 in __sprintf (sprintf.c)
/home/tibbo/Dev/Project/DynLoader/src/main.zig:26:16: 0x11a1e4f in main (main.zig)
    _ = sprintf(&buf, @ptrFromInt(0x8));
               ^
/home/tibbo/Builds/Zig/zig-x86_64-linux-0.16.0-dev.1225+bf9082518/lib/std/start.zig:696:37: 0x11a2523 in callMain (std.zig)
            const result = root.main() catch |err| {
                                    ^
Unwind error at address `/proc/self/exe:0x11a2523` (unwind info invalid), remaining frames may be incorrect
/home/tibbo/Builds/Zig/zig-x86_64-linux-0.16.0-dev.1225+bf9082518/lib/std/start.zig:237:5: 0x11889a1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
[1]    15724 IOT instruction  zig run src/main.zig

An almost perfect stack trace from a segfault in a dynamically loaded library! There is still an unwind error that I need to debug, but the ability to get a stack trace was extremely useful.

I consider the prototyping almost done and will soon proceed to a complete rewrite, as I now have a much clearer view of the subject. I just want to open a window and display something using X11 and EGL before that.

If you are curious, you can find the current dynamic_library_loader.zig file here. For the prototyping phase I decided to put everything into this one file.


How does it work?

Here is a breakdown of the main steps:

  • Map the .so library ELF file
  • Parse the ELF file
    • Collect every symbol, with versions, visibility, binding, etc.
    • Collect PT_LOAD segments
    • Read PT_GNU_RELRO segments to collect the final permissions
    • Collect the PT_TLS segment to handle thread-local storage later
    • Read the PT_DYNAMIC segment
      • Collect dependencies from DT_NEEDED
      • Collect relocations
        • Even the RELR ones, for which documentation is very sparse, but still used by every dynamic library on my system…
      • Collect DT_INIT, DT_INIT_ARRAY, DT_FINI, DT_FINI_ARRAY
  • Map PT_LOAD segments, trying to handle the file offset vs. memory offset mess, and honor permissions
  • Repeat the previous steps with all discovered dependencies if needed
  • For each newly loaded library, handle TLS
    • Set the new TLS area. The initial TLS area is set by the zig startup process before main. It must be resized, adding new TLS blocks for each library that needs it, so we gather info about the current one using std.os.tls.area_desc. But there is a problem: in addition to extending the area to lower addresses to place the new TLS block, we also extend it toward higher addresses to give room for the libc pthread structure. More on this after.
    • Set the new thread pointer (the fs register), and set the first word (as required by the ABI) and the third word (to respect the pthread struct layout) after TP to the TP address.
    • Save the offset of the new TLS block for later.
  • Process normal relocations
    • For TPOFF64 relocations, we use the TLS offset saved previously.
    • For the JUMP_SLOT ones, we check before applying them if the symbol is a function that should be handled by ourselves, like dlopen, because we are the dynamic loader now. A function that is part of the public API of libc’s ld should be implemented by us.
  • Process IRELATIVE relocations, in a separate pass because those resolver functions can depend on other relocations.
  • Update permissions to their final state now that relocations have been applied.
  • Call the function from DT_INIT and the functions from DT_INIT_ARRAY, taking care of the fact that the first init function of libc expects argc, argv, and the environment. A fact that I discovered only by reading glibc source code…

About TLS

When zig initializes the TLS area for the current static executable, it uses this layout on linux x86-64:

-----------------------------------------------
| TLS Blocks | ABI TCB | Zig TCB | DTV struct |
-------------^---------------------------------
              `-- The TP register points here.

A more convenient layout, when loading libc, would be something like this:

                       | POTENTIAL PTHREAD STRUCT ====>
---------------------------------------------------------
| TLS Blocks | Zig TCB | ABI TCB | *DTV | *SELF | SPACE
-----------------------^---------------------------------
                       `-- The TP register points here.

just in case alexrp sees this

When mapping a TLS block from a loaded library, I compute the size of the new area using the _thread_db_sizeof_pthread symbol (I get rid of the Zig TCB struct for now), just in case. I feel so lucky that std.os.tls.area_desc is a pub var. And it seems I will eventually have a good reason to implement __tls_get_addr :slight_smile:


About custom SelfInfo

Unfortunately, it seems there is no convenient way to add modules to be parsed when unwinding using the default implementation of SelfInfo (lib/std/debug/SelfInfo/Elf.zig).

just in case mlugg sees this

But I have to say, making SelfInfo customizable is pure genius. I just copied the default implementation, added an extra_phdr_infos array, and adapted the findModule logic to use it. With #25668 merged, the unwinding process is able to print a full stack trace! Again, it was immensely useful.


Just a final question: should I continue to post updates here? (I don’t know if it is considered as necroposting).

And apologies if my English sounds a bit off.

17 Likes

There are many threads about dynamic linking… I think your epic work deserves a Thread of its own!

2 Likes

@Joen-UnLogick Scrolling over the history of this topic, I think this is @TibboddiT’s topic (they created it and contributed a lot to the direction it is going).

Topics and posting

So I would say keep posting here if you want to, I think it is nice to be able to read the full history of what you have worked on here, it seems to me that splitting it into a new topic is unnecessary.

That said you also could start a new topic quoting/linking to this topic, for example if you want to make a showcase topic of your implementation / project, to show the results of your rewrite and have that topic parallel to this one.

No, we just want topics to align with their content, so you returning to continue your interesting posts is very much aligned. Just because a topic is a bit older, doesn’t mean that it is finished, but if topics drift away from their original direction too much, we usually want that conversion to be split into a new topic (So that the topic title stays relevant and it isn’t too confusing to read).

Something that is less wanted is when people hijack help topics that are done, that where started by other people, just because they have a similar problem, instead of creating their own topic and maybe referring to the other topic (if it is relevant). So it also depends a bit on the category. People also can flag posts as off-topic to let us know about posts that drift away too much from the topic.

1 Like

Thanks, noted.

This thread kinda already is dedicated to it, so I’ll post updates here :wink:

3 Likes

This is some awesome work! I intend to try out your prototype this weekend, and see how far I can get with loading libvulkan. I’m interested to see where this project goes.

2 Likes

Nice! I also intend to make the X11 / EGL thingy work this weekend. Let me know if run into any problems, as I’ll be able to fix them almost in real time :slight_smile:

Edit: I just tried loading libvulkan.so.1, and it loaded fine, and the 2 functions in init_array were called successfully (I didn’t try to use it though).

Just as an aside, Alex, I’m constantly in awe at the speed of your programming, and how fast you are to help the community. A few weeks ago I asked some basic questions about some incredibly niche architecture not working for Zig, and not only did you take the time to explain it to me, you put in a whole PR to get some features for Zig+Arc working a little more smoothly.

Thank you for being such an awesome part of the OSS community - you make me want to contribute to Zig (and I plan to soon!).

8 Likes

I expect it will be a bit more tricky to actually use it, as libvulkan.so.1 by itself is basically just a wrapper around dlopen. It contains logic to select which GPU driver to load, but AFAIK it doesn’t do much itself.

I see. Here is relevant information:

readelf -sW /usr/lib/x86_64-linux-gnu/libvulkan.so.1 | grep ' dl'   [0:17]
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlerror@GLIBC_2.34 (5)
    23: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dladdr@GLIBC_2.34 (5)
    38: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlopen@GLIBC_2.34 (5)
    54: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlsym@GLIBC_2.34 (5)
    58: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlclose@GLIBC_2.34 (5)

I plan to add implementations for these functions to the prototype tomorrow. By chance, those are the “easy” ones. Except for dlclose, but I guess a compliant implementation will not actually be required, because AFAIK dlclosing is generally really necessary for hot reloading use cases.

I’ll update the gist and let you know when it’s done.

1 Like

Okay, I ended trying it out today. Slight curveball, I run Chimera Linux, which is musl libc based. The first thing I had to handle was that Chimera’s /usr/lib/libc.so does not have a versym table. I changed the code to handle missing versym data (this diff is longer than it needs to be, most of it is indenting the version code a level):

diff --git a/dynamic_loader_library.zig b/dynamic_loader_library.zig
index 3fdd741bc9..f9d11c5522 100644
--- a/dynamic_loader_library.zig
+++ b/dynamic_loader_library.zig
@@ -1311,7 +1311,7 @@
     const sh_strtab_addr: usize = file_addr + sh_strtbl.sh_offset;
 
     var dyn_strtab_addr: usize = undefined;
-    var versym_tab_addr: usize = undefined;
+    var versym_tab_addr: ?usize = null;
     var verdef_tab_addr: usize = undefined;
     var verneed_tab_addr: usize = undefined;
 
@@ -1410,9 +1410,11 @@
         }
     }
 
-    Logger.debug("versym table addr: 0x{x}", .{versym_tab_addr});
-
-    const versym_table: [*]std.elf.Half = @ptrFromInt(versym_tab_addr);
+    const versym_table: ?[*]std.elf.Half =
+        if (versym_tab_addr) |addr| blk: {
+            Logger.debug("versym table addr: 0x{x}", .{addr});
+            break :blk @ptrFromInt(addr);
+        } else null;
 
     Logger.debug("dynamic string table addr: 0x{x}", .{dyn_strtab_addr});
     Logger.debug("dynamic sym table addr: 0x{x}", .{dyn_symtab_addr});
@@ -1423,106 +1425,106 @@
         const sym: *std.elf.Sym = @ptrFromInt(dyn_symtab_addr + j * @sizeOf(std.elf.Sym));
 
         // TODO max len of sym name / aux name ?
-        var buf_str: [2048]u8 = @splat(0);
         const strs: [*]u8 = @ptrFromInt(dyn_strtab_addr);
         var k: usize = 0;
-        while (strs[k + sym.st_name] != 0) : (k += 1) {
-            buf_str[k] = strs[k + sym.st_name];
-        }
-
-        const name = try std.fmt.allocPrint(allocator, "{s}", .{buf_str[0..k]});
-
-        const ver_sym: std.elf.Versym = @bitCast(versym_table[j]);
-        const ver_idx = ver_sym.VERSION;
-        const hidden = ver_sym.HIDDEN;
-
-        var version: []const u8 = undefined;
-
-        if (ver_sym == std.elf.Versym.GLOBAL) {
-            version = try allocator.dupe(u8, "GLOBAL");
-        } else if (ver_sym == std.elf.Versym.LOCAL) {
-            version = try allocator.dupe(u8, "LOCAL");
-        } else {
-            if (sym.st_shndx == @intFromEnum(@as(SHN, .SHN_UNDEF))) {
-                const ver_table_addr = verneed_tab_addr;
-
-                var ver_table_cursor = ver_table_addr;
-                var curr_def: *std.elf.Elf64_Verneed = @ptrFromInt(ver_table_cursor);
-
-                // logger.debug("searching ver_idx: {d}", .{ver_idx});
-
-                outer: while (true) {
-                    var aux: *std.elf.Vernaux = @ptrFromInt(ver_table_cursor + curr_def.vn_aux);
+        while (strs[k + sym.st_name] != 0) : (k += 1) {}
+        const name = strs[sym.st_name .. sym.st_name + k];
+
+        // const name = try std.fmt.allocPrint(allocator, "{s}", .{buf_str[0..k]});
+
+        var version: []const u8 = "";
+
+        if (versym_table) |versym_tab| {
+            const ver_sym: std.elf.Versym = @bitCast(versym_tab[j]);
+            const ver_idx = ver_sym.VERSION;
+            if (ver_sym == std.elf.Versym.GLOBAL) {
+                version = "GLOBAL";
+            } else if (ver_sym == std.elf.Versym.LOCAL) {
+                version = "LOCAL";
+            } else {
+                if (sym.st_shndx == @intFromEnum(@as(SHN, .SHN_UNDEF))) {
+                    const ver_table_addr = verneed_tab_addr;
+
+                    var ver_table_cursor = ver_table_addr;
+                    var curr_def: *std.elf.Elf64_Verneed = @ptrFromInt(ver_table_cursor);
+
+                    // logger.debug("searching ver_idx: {d}", .{ver_idx});
+
+                    outer: while (true) {
+                        var aux: *std.elf.Vernaux = @ptrFromInt(ver_table_cursor + curr_def.vn_aux);
+                        while (true) {
+                            if (aux.other == ver_idx) {
+                                k = 0;
+                                while (strs[k + aux.name] != 0) : (k += 1) {
+                                    // buf_str[k] = strs[k + aux.name];
+                                }
+                                version = strs[aux.name .. aux.name + k];
+
+                                break :outer;
+                            }
+
+                            // logger.debug("skipping ver_idx: {d}", .{aux.other});
+
+                            if (aux.next == 0) {
+                                break;
+                            }
+
+                            // logger.debug("next aux offset: {d}", .{aux.next});
+                            aux = @ptrFromInt(@intFromPtr(aux) + aux.next);
+                        }
+
+                        if (curr_def.vn_next == 0) {
+                            Logger.err("symbol version {d} not found", .{ver_idx});
+                            return error.SymbolVersionNotFound;
+                        }
+
+                        ver_table_cursor += curr_def.vn_next;
+                        // logger.debug("ver table_cursor: 0x{x}: {d}", .{ ver_table_cursor, curr_def.vn_next });
+                        curr_def = @ptrFromInt(ver_table_cursor);
+                    }
+                } else {
+                    const ver_table_addr = verdef_tab_addr;
+
+                    var ver_table_cursor = ver_table_addr;
+                    var curr_def: *std.elf.Verdef = @ptrFromInt(ver_table_cursor);
+
+                    // logger.debug("searching ver_idx: {d}", .{ver_idx});
+
                     while (true) {
-                        if (aux.other == ver_idx) {
+                        if (curr_def.ndx == @as(std.elf.VER_NDX, @enumFromInt(ver_idx))) {
+                            const aux: *std.elf.Verdaux = @ptrFromInt(ver_table_cursor + curr_def.aux);
+
                             k = 0;
-                            while (strs[k + aux.name] != 0) : (k += 1) {
-                                buf_str[k] = strs[k + aux.name];
-                            }
-
-                            version = try std.fmt.allocPrint(allocator, "{s}", .{buf_str[0..k]});
-                            break :outer;
-                        }
-
-                        // logger.debug("skipping ver_idx: {d}", .{aux.other});
-
-                        if (aux.next == 0) {
+                            while (strs[k + aux.name] != 0) : (k += 1) {}
+
+                            version = strs[aux.name .. aux.name + k];
                             break;
                         }
 
-                        // logger.debug("next aux offset: {d}", .{aux.next});
-                        aux = @ptrFromInt(@intFromPtr(aux) + aux.next);
-                    }
-
-                    if (curr_def.vn_next == 0) {
-                        Logger.err("symbol version {d} not found", .{ver_idx});
-                        return error.SymbolVersionNotFound;
-                    }
-
-                    ver_table_cursor += curr_def.vn_next;
-                    // logger.debug("ver table_cursor: 0x{x}: {d}", .{ ver_table_cursor, curr_def.vn_next });
-                    curr_def = @ptrFromInt(ver_table_cursor);
-                }
-            } else {
-                const ver_table_addr = verdef_tab_addr;
-
-                var ver_table_cursor = ver_table_addr;
-                var curr_def: *std.elf.Verdef = @ptrFromInt(ver_table_cursor);
-
-                // logger.debug("searching ver_idx: {d}", .{ver_idx});
-
-                while (true) {
-                    if (curr_def.ndx == @as(std.elf.VER_NDX, @enumFromInt(ver_idx))) {
-                        const aux: *std.elf.Verdaux = @ptrFromInt(ver_table_cursor + curr_def.aux);
-
-                        k = 0;
-                        while (strs[k + aux.name] != 0) : (k += 1) {
-                            buf_str[k] = strs[k + aux.name];
+                        if (curr_def.next == 0) {
+                            Logger.err("symbol version {d} not found", .{ver_idx});
+                            return error.SymbolVersionNotFound;
                         }
 
-                        version = try std.fmt.allocPrint(allocator, "{s}", .{buf_str[0..k]});
-                        break;
-                    }
-
-                    if (curr_def.next == 0) {
-                        Logger.err("symbol version {d} not found", .{ver_idx});
-                        return error.SymbolVersionNotFound;
-                    }
-
-                    // logger.debug("skipping ver_idx: {d}", .{@intFromEnum(curr_def.ndx)});
-
-                    ver_table_cursor += curr_def.next;
-                    // logger.debug("ver table_cursor: 0x{x}: {d}", .{ ver_table_cursor, curr_def.next });
-                    curr_def = @ptrFromInt(ver_table_cursor);
+                        // logger.debug("skipping ver_idx: {d}", .{@intFromEnum(curr_def.ndx)});
+
+                        ver_table_cursor += curr_def.next;
+                        // logger.debug("ver table_cursor: 0x{x}: {d}", .{ ver_table_cursor, curr_def.next });
+                        curr_def = @ptrFromInt(ver_table_cursor);
+                    }
                 }
             }
         }
 
         Logger.debug("  - {d}:", .{j});
         Logger.debug("    name: {s}", .{name});
-        Logger.debug("    ver idx: {d}", .{ver_sym.VERSION});
         Logger.debug("    version: {s}", .{version});
-        Logger.debug("    hidden: {}", .{hidden});
+        if (versym_table) |versym_tab| {
+            const ver_sym: std.elf.Versym = @bitCast(versym_tab[j]);
+            const hidden = ver_sym.HIDDEN;
+            Logger.debug("    ver idx: {d}", .{ver_sym.VERSION});
+            Logger.debug("    hidden: {}", .{hidden});
+        }
         Logger.debug("    raw type: 0x{x}", .{sym.st_type()});
         Logger.debug("    type: {s}", .{@tagName(@as(STT, @enumFromInt(sym.st_type())))});
         Logger.debug("    bind: {s}", .{@tagName(@as(STB, @enumFromInt(sym.st_bind())))});
@@ -1534,7 +1536,10 @@
         const s: DynSym = .{
             .name = name,
             .version = version,
-            .hidden = hidden,
+            .hidden = if (versym_table) |versym_tab| blk: {
+                const ver_sym: std.elf.Versym = @bitCast(versym_tab[j]);
+                break :blk ver_sym.HIDDEN;
+            } else false,
             .offset = j * @sizeOf(std.elf.Sym),
             .type = @as(STT, @enumFromInt(sym.st_type())),
             .bind = @as(STB, @enumFromInt(sym.st_bind())),

With that change I was able to get the sprintf example running, though the debug info is missing.

After that I created a vulkan_version example which just gets the version using vkEnumerateInstanceVersion:

pub const debug = struct {
    pub const SelfInfo = ld.CustomSelfInfo;
};

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{ .stack_trace_frames = 10 }) = .init;
    const allocator = gpa.allocator();
    defer if (gpa.deinit() != .ok) @panic("Memory check failed");

    try ld.init(.{ .debug = true });
    defer ld.deinit(allocator);

    const lib = try ld.load(allocator, "libvulkan.so.1");

    const vkEnumerateInstanceVersion_sym = try lib.getSymbol("vkEnumerateInstanceVersion");
    const vkEnumerateInstanceVersion_addr = vkEnumerateInstanceVersion_sym.addr;
    const vkEnumerateInstanceVersion: *const fn (*u32) callconv(.c) c_int = @ptrFromInt(vkEnumerateInstanceVersion_addr);

    var vk_version: u32 = 0;
    switch (vkEnumerateInstanceVersion(&vk_version)) {
        0 => std.log.info("vulkan version = {}", .{vk_version}),
        else => |e| std.log.info("error getting vulkan version = 0x{x}", .{e}),
    }
}

const ld = @import("ld");
const std = @import("std");

Unfortunately, it seems to run into an issue in the init code. Running it in lldb gives some more info:

* thread #1, name = 'vulkan_version', stop reason = signal SIGSEGV: invalid permissions for mapped object (fault address=0x7ffff7ada39f)

It seems like the segment has the wrong permissions? Also, as far as I can tell the address it’s getting isn’t inside the segments, but I could be reading those log messages wrong.

EDIT: Adding some more context since the gist got cut off:

libvulkan segments:

debug(dynamic_library_loader): processing IRELATIVE relocations for libvulkan.so.1
debug(dynamic_library_loader): processed IRELATIVE relocations for libvulkan.so.1
debug(dynamic_library_loader): processing IRELATIVE relocations for libc.so
debug(dynamic_library_loader): processed IRELATIVE relocations for libc.so
debug(dynamic_library_loader): updating segment permissions for libvulkan.so.1
debug(dynamic_library_loader):   updating segment 0: from 0x7ffff7a1c000 to 0x7ffff7a4d000, prot: 0x1
debug(dynamic_library_loader):   updating segment 1: from 0x7ffff7a4e000 to 0x7ffff7a99000, prot: 0x5
debug(dynamic_library_loader):   updating segment 2: from 0x7ffff7a9b000 to 0x7ffff7a9d000, prot: 0x1
debug(dynamic_library_loader):   updating segment 3: from 0x7ffff7a9e000 to 0x7ffff7a9e000, prot: 0x3
debug(dynamic_library_loader): successfully updated 4 segments for libvulkan.so.1
debug(dynamic_library_loader): updating segment permissions for libc.so
debug(dynamic_library_loader):   updating segment 0: from 0x7ffff7621000 to 0x7ffff766b000, prot: 0x1
debug(dynamic_library_loader):   updating segment 1: from 0x7ffff766d000 to 0x7ffff76ee000, prot: 0x5
debug(dynamic_library_loader):   updating segment 2: from 0x7ffff76ee000 to 0x7ffff76f0000, prot: 0x1
debug(dynamic_library_loader):   updating segment 3: from 0x7ffff76f0000 to 0x7ffff779e000, prot: 0x3
debug(dynamic_library_loader): successfully updated 4 segments for libc.so
info(dynamic_library_loader): name: libvulkan.so.1
info(dynamic_library_loader):   path: /lib64/libvulkan.so.1
info(dynamic_library_loader):   mapped_at: 0x7ffff7c5f000
info(dynamic_library_loader):   segments:
info(dynamic_library_loader):     4 segments loaded
info(dynamic_library_loader):     - 1:
info(dynamic_library_loader):       file_offset: 0x0
info(dynamic_library_loader):       file_size: 0x31a94
info(dynamic_library_loader):       mem_offset: 0x0
info(dynamic_library_loader):       mem_size: 0x31a94
info(dynamic_library_loader):       mem_align: 0x1000
info(dynamic_library_loader):       loadedAt: 0x7ffff7a1c000
info(dynamic_library_loader):       flags_first: R
info(dynamic_library_loader):       flags_last: R
info(dynamic_library_loader):     - 2:
info(dynamic_library_loader):       file_offset: 0x31aa0
info(dynamic_library_loader):       file_size: 0x4bcb0
info(dynamic_library_loader):       mem_offset: 0x32aa0
info(dynamic_library_loader):       mem_size: 0x4bcb0
info(dynamic_library_loader):       mem_align: 0x1000
info(dynamic_library_loader):       loadedAt: 0x7ffff7a4eaa0
info(dynamic_library_loader):       flags_first: RX
info(dynamic_library_loader):       flags_last: RX
info(dynamic_library_loader):     - 3:
info(dynamic_library_loader):       file_offset: 0x7d750
info(dynamic_library_loader):       file_size: 0x2730
info(dynamic_library_loader):       mem_offset: 0x7f750
info(dynamic_library_loader):       mem_size: 0x28b0
info(dynamic_library_loader):       mem_align: 0x1000
info(dynamic_library_loader):       loadedAt: 0x7ffff7a9b750
info(dynamic_library_loader):       flags_first: RW
info(dynamic_library_loader):       flags_last: R
info(dynamic_library_loader):     - 4:
info(dynamic_library_loader):       file_offset: 0x7fe80
info(dynamic_library_loader):       file_size: 0x3b
info(dynamic_library_loader):       mem_offset: 0x82e80
info(dynamic_library_loader):       mem_size: 0x1a8
info(dynamic_library_loader):       mem_align: 0x1000
info(dynamic_library_loader):       loadedAt: 0x7ffff7a9ee80
info(dynamic_library_loader):       flags_first: RW
info(dynamic_library_loader):       flags_last: RW
info(dynamic_library_loader):   tls_init_file_offset: 0x0
info(dynamic_library_loader):   tls_init_file_size: 0x0
info(dynamic_library_loader):   tls_init_mem_offset: 0x0
info(dynamic_library_loader):   tls_init_mem_size: 0x0
info(dynamic_library_loader):   init/fini:
info(dynamic_library_loader):     init_addr: 0x7e39f
info(dynamic_library_loader):     fini_addr: 0x7e3a2
info(dynamic_library_loader):     init_array_addr: 0x7f750, size: 0x10
info(dynamic_library_loader):     fini_array_addr: 0x7f760, size: 0x10

Message right before segmentation fault and backtrace from lldb:

info(dynamic_library_loader): _dl_debug_state
info(dynamic_library_loader): calling init function for libc.so at 0x7ffff7678b10 (initial address: 0x7ffff79a2b10)
info(dynamic_library_loader): calling init function for libvulkan.so.1 at 0x7ffff7a9a39f (initial address: 0x7ffff7cdc39f)
Process 21278 stopped
* thread #1, name = 'vulkan_version', stop reason = signal SIGSEGV: invalid permissions for mapped object (fault address=0x7ffff7a9a39f)
    frame #0: 0x00007ffff7a9a39f
->  0x7ffff7a9a39f: pushq  %rax
    0x7ffff7a9a3a0: popq   %rax
    0x7ffff7a9a3a1: retq   
    0x7ffff7a9a3a2: pushq  %rax
(lldb) bt
* thread #1, name = 'vulkan_version', stop reason = signal SIGSEGV: invalid permissions for mapped object (fault address=0x7ffff7a9a39f)
  * frame #0: 0x00007ffff7a9a39f
    frame #1: 0x000000000119ac8c vulkan_version`dynamic_loader_library.callInitFunctions at dynamic_loader_library.zig:2613:13
    frame #2: 0x000000000119cd7b vulkan_version`dynamic_loader_library.load at dynamic_loader_library.zig:1129:30
    frame #3: 0x000000000119e6ac vulkan_version`vulkan_version.main at vulkan_version.zig:13:28
    frame #4: 0x000000000119efe4 vulkan_version`start.posixCallMainAndExit at start.zig:696:37
    frame #5: 0x00000000011854e2 vulkan_version`start._start at start.zig:237:5
1 Like

Thanks for the test :slight_smile:

I will check tomorrow, but I wanted to let you know that on my machine, with latest version (I updated the gist), I get this with from your example:

$  zig run src/test.zig

...

info(dynamic_library_loader): _dl_debug_state
info(dynamic_library_loader): calling 3 init_array functions for libc.so.6 (0x1e3b68)
info(dynamic_library_loader): calling libc init_array[0] for libc.so.6 at 0x7f1059313a20 (initial address: 0x29a20)
info(dynamic_library_loader): calling libc init_array[1] for libc.so.6 at 0x7f1059313a90 (initial address: 0x29a90)
info(dynamic_library_loader): calling libc init_array[2] for libc.so.6 at 0x7f1059313b00 (initial address: 0x29b00)
info(dynamic_library_loader): calling init function for libvulkan.so.1 at 0x7f1059dbc000 (initial address: 0x8000)
info(dynamic_library_loader): calling 2 init_array functions for libvulkan.so.1 (0x8a120)
info(dynamic_library_loader): calling init_array[0] for libvulkan.so.1 at 0x7f1059dbc500 (initial address: 0x8500)
info(dynamic_library_loader): calling init_array[1] for libvulkan.so.1 at 0x7f1059dbc440 (initial address: 0x8440)
info: error getting vulkan version = 0x-1

I will use your example as my main test, and will keep you up to date.

1 Like

Oh sweet! It’s returning an error without crashing :smiley:

I also figured out why it was crashing on my system; when mprotecting the segments the end address math was wrong, so the end of the segment would have the wrong permissions.

Commit ID: 192d47b3eef1cf4e7a4ab2f3e1fece021637cc7a
Change ID: olprwzrlxmlwrmtozmtplmlluqvplxkx
Author   : geemili <opensource@geemili.xyz> (2025-11-07 22:22:37)
Committer: geemili <opensource@geemili.xyz> (2025-11-07 22:24:02)

    fix end address math for mprotecting segments

diff --git a/dynamic_loader_library.zig b/dynamic_loader_library.zig
index 8578f2bd63..6c90243075 100644
--- a/dynamic_loader_library.zig
+++ b/dynamic_loader_library.zig
@@ -2531,7 +2531,7 @@
         if (segment.flags_last.exec) prot |= std.posix.PROT.EXEC;
 
         const aligned_start = std.mem.alignBackward(usize, segment.loaded_at + (segment.flags_last.mem_offset - segment.mem_offset), std.heap.pageSize());
-        const aligned_end = std.mem.alignBackward(usize, aligned_start + segment.flags_last.mem_size, std.heap.pageSize());
+        const aligned_end = std.mem.alignForward(usize, segment.loaded_at + segment.flags_last.mem_size, std.heap.pageSize());
         Logger.debug("  updating segment {d}: from 0x{x} to 0x{x}, prot: 0x{x}", .{ s, aligned_start, aligned_end, prot });
 
         const segment_slice = @as([*]align(std.heap.pageSize()) u8, @ptrFromInt(aligned_start))[0 .. aligned_end - aligned_start];

EDIT: Here’s the error I run into now:

info(dynamic_library_loader): calling init function for libc.so at 0x7b9bef3b8b10 (initial address: 0x7b9bef6e2b10)
info(dynamic_library_loader): calling init function for libvulkan.so.1 at 0x7b9bef7da39f (initial address: 0x7b9befa1c39f)
info(dynamic_library_loader): calling 2 init_array functions for libvulkan.so.1 (0x7f750)
info(dynamic_library_loader): calling init_array[0] for libvulkan.so.1 at 0x7b9bef78eaa0 (initial address: 0x32aa0)
info(dynamic_library_loader): calling init_array[1] for libvulkan.so.1 at 0x7b9bef7b86a0 (initial address: 0x5c6a0)
General protection exception (no address available)
???:?:?: 0x7b9bef417725 in ??? (???)
Unwind error at address `???:0x7b9bef417725` (unwind info unavailable), remaining frames may be incorrect
???:?:?: 0x7b9bef7cb66a in ??? (???)
???:?:?: 0x7b9bef7cbff5 in ??? (???)
???:?:?: 0x7b9bef7cee45 in ??? (???)
/home/geemili/code/klaji/dynamic-loader/examples/vulkan_version.zig:20:39: 0x119e8a3 in main (vulkan_version.zig)
    switch (vkEnumerateInstanceVersion(&vk_version)) {
                                      ^
/home/geemili/.local/share/ziglang/0.16.0-dev.1216+846854972/lib/std/start.zig:696:37: 0x119efe3 in callMain (std.zig)
            const result = root.main() catch |err| {
                                    ^
/home/geemili/.local/share/ziglang/0.16.0-dev.1216+846854972/lib/std/start.zig:237:5: 0x11854e1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
run:vulkan_version
└─ run exe vulkan_version failure
error: process terminated unexpectedly
failed command: ./.zig-cache/o/74f622e7620ae4ea5de5c4a86673cefd/vulkan_version
1 Like

Arrgghh, sorry about that… good catch anyway! But I think it should be

const aligned_end = std.mem.alignForward(usize, segment.loaded_at + (segment.flags_last.mem_offset - segment.mem_offset) + segment.flags_last.mem_size, std.heap.pageSize());

After reviewing the logs you posted, it seems that the segments mapping take the “multi” path, where segment are memcopied to an anonymously mapped memory. This is “legacy” code, and it explains why you don’t get a full stack trace.

So now it segfault actually inside the lib. I think you should first focus on getting stack traces. Can you send your libvulkan.so file (or a link to the file hosted somewhere, package manager for example)? I will update the segments mapping process later today.

Also, if not done already, you should inspect the last revision of the gist to port the changes on your modified version. I discovered that IRELATIVE relocations resolver results should be used when resolving symbols after having applied them.

2 Likes

I updated the gist with legacy mapping code removed, and made improvements to IRELOCATIONS handling. Hopefully with those changes applied you will have a stacktrace (keep in mind that your changes relative to the versym table has stiil not been integrated). I plan to do it later today.

I now have a segfault on a memcpy call when vulkan try to read a json file (/usr/share/vulkan/implicit_layer.d/VkLayer_MESA_device_select.json):

Segmentation fault at address 0x7fe3b4754140
../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:838:0: 0x7fe3b4b18400 in __memcpy_sse2_unaligned_erms (../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S)
../string/bits/string_fortified.h:29:10: 0x7fe3b4af59da in memcpy (fileops.c)
./libio/iofread.c:38:16: 0x7fe3b4ae8e68 in _IO_fread (iofread.c)
???:?:?: 0x7fe3b4791f99 in ??? (/lib/x86_64-linux-gnu/libvulkan.so.1)
???:?:?: 0x7fe3b478a790 in ??? (/lib/x86_64-linux-gnu/libvulkan.so.1)
???:?:?: 0x7fe3b478aaa7 in ??? (/lib/x86_64-linux-gnu/libvulkan.so.1)
???:?:?: 0x7fe3b479782c in ??? (/lib/x86_64-linux-gnu/libvulkan.so.1)
/home/tibbo/Dev/Project/DynLoader/src/vk.zig:27841:60: 0x11ac568 in createInstance (main.zig)
            const result = self.dispatch.vkCreateInstance.?(
                                                           ^
/home/tibbo/Dev/Project/DynLoader/src/vulkan.zig:40:44: 0x11a241c in test_vulkan (main.zig)
    const instance = try vkb.createInstance(&.{
                                           ^
/home/tibbo/Dev/Project/DynLoader/src/main.zig:39:27: 0x11a2b03 in main (main.zig)
    try vulkan.test_vulkan(allocator);

Reading the source code in memmove-vec-unaligned-erms.S I understand the segfault (the outer loop counter of large_memcpy_4x is underflowing, and so the loop never exits) but I still have no clue why. I will try to fix that before anything else (including implementing dl public API).

1 Like

EDIT: Just saw your most recent reply.

No problem, thank you for your efforts :smiley:

~/tmp/apk-extract 
❯ wcurl https://repo.chimera-linux.org/current/main/x86_64/vulkan-loader-1.4.326-r0.apk
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  149k  100  149k    0     0  73888      0  0:00:02  0:00:02 --:--:-- 73912

~/tmp/apk-extract took 2s 
❯ apk extract ./vulkan-loader-1.4.326-r0.apk
Extracting ./vulkan-loader-1.4.326-r0.apk...

~/tmp/apk-extract 
❯ fd
usr/
usr/lib/
usr/lib/libvulkan.so.1
usr/lib/libvulkan.so.1.4.326
vulkan-loader-1.4.326-r0.apk

I also made a hacky patch trying to fix the debug info, but I’m pretty sure I’m doing the conversion from loaded address to virtual address wrong.

Commit ID: 87c22b8201ac9af28f6fb3a46a6ec2717efd3a0a
Change ID: wvkpsvsqkysymwmoyzmtlswoutyluuro
Bookmarks: multi-segment-debug multi-segment-debug@git multi-segment-debug@origin
Author   : geemili <opensource@geemili.xyz> (2025-11-08 09:58:20)
Committer: geemili <opensource@geemili.xyz> (2025-11-08 11:08:07)

    hacky patch to support debug info for multi-segment elf files

diff --git a/dynamic_loader_library.zig b/dynamic_loader_library.zig
index 560c081aee..a6d3ba2da6 100644
--- a/dynamic_loader_library.zig
+++ b/dynamic_loader_library.zig
@@ -53,6 +53,30 @@
         const module = try si.findModule(gpa, address, .exclusive);
         defer si.rwlock.unlock();
 
+        if (dyn_objects.get(module.name)) |dyn_object| {
+            var ret_symbol: std.debug.Symbol = .unknown;
+            ret_symbol.compile_unit_name = dyn_object.name;
+            for (dyn_object.segments.values()) |seg| {
+                if (address < seg.loaded_at or address >= seg.loaded_at + seg.mem_size) {
+                    continue;
+                }
+                const vaddr = address - seg.loaded_at + seg.mem_offset;
+                // dyn_object.syms.
+
+                for (dyn_object.syms_array.items) |dep_sym| {
+                    if (dep_sym.shidx == .SHN_ABS) {
+                        Logger.debug("WARNING: ABSOLUTE SYMBOL from dep: {s}", .{dep_sym.name});
+                    }
+
+                    if (vaddr < dep_sym.value or vaddr >= dep_sym.value + dep_sym.size) continue;
+
+                    ret_symbol.name = dep_sym.name;
+                    break;
+                }
+            }
+            return ret_symbol;
+        }
+
         const vaddr = address - module.load_offset;
 
         const loaded_elf = try module.getLoadedElf(gpa);
@@ -428,8 +452,28 @@
             var ctx: DlIterContext = .{ .si = si, .gpa = gpa };
             try std.posix.dl_iterate_phdr(&ctx, error{OutOfMemory}, DlIterContext.callback);
 
-            for (extra_phdr_infos.items) |info| {
-                try DlIterContext.callback(info, @sizeOf(std.posix.dl_phdr_info), &ctx);
+            // for (extra_phdr_infos.items) |info| {
+            //     try DlIterContext.callback(info, @sizeOf(std.posix.dl_phdr_info), &ctx);
+            // }
+            for (dyn_objects.values()) |*dyn_object| {
+                const module_index = si.modules.items.len;
+                try si.modules.append(gpa, .{
+                    .load_offset = 0,
+                    // Android libc uses NULL instead of "" to mark the main program
+                    .name = dyn_object.name,
+                    .build_id = null,
+                    .gnu_eh_frame = null,
+                    .unwind = null,
+                    .loaded_elf = null,
+                });
+
+                for (dyn_object.segments.values()) |segment| {
+                    try si.ranges.append(gpa, .{
+                        .start = segment.loaded_at,
+                        .len = segment.mem_size,
+                        .module_index = module_index,
+                    });
+                }

It at least puts the module name in the stack trace:

General protection exception (no address available)
???:?:?: 0x71dfbd2b76e5 in ??? (libc.so)
Unwind error at address `libc.so:0x71dfbd2b76e5` (unwind info unavailable), remaining frames may be incorrect
???:?:?: 0x71dfbd64b66a in ??? (libvulkan.so.1)
???:?:?: 0x71dfbd64bff5 in ??? (libvulkan.so.1)
???:?:?: 0x71dfbd64ee45 in vkEnumerateInstanceVersion (libvulkan.so.1)
/home/geemili/code/klaji/dynamic-loader/examples/vulkan_version.zig:20:39: 0x11a3823 in main (vulkan_version.zig)
    switch (vkEnumerateInstanceVersion(&vk_version)) {
                                      ^
/home/geemili/.local/share/ziglang/0.16.0-dev.1216+846854972/lib/std/start.zig:696:37: 0x11a3f63 in callMain (std.zig)
            const result = root.main() catch |err| {
                                    ^
/home/geemili/.local/share/ziglang/0.16.0-dev.1216+846854972/lib/std/start.zig:237:5: 0x1189e61 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
run:vulkan_version
└─ run exe vulkan_version failure
error: process terminated unexpectedly
failed command: ./.zig-cache/o/09bcfd7542e87c1557e5c23c659dbf98/vulkan_version

Build Summary: 1/3 steps succeeded (1 failed)
run:vulkan_version transitive failure
└─ run exe vulkan_version failure

error: the following build command failed with exit code 1:
.zig-cache/o/d975885aa5550873cf117d9404d8dbc8/build /home/geemili/.local/share/ziglang/0.16.0-dev.1216+846854972/zig /home/geemili/.local/share/ziglang/0.16.0-dev.1216+846854972/lib /home/geemili/code/klaji/dynamic-loader .zig-cache /home/geemili/.cache/zig --seed 0x23e80b68 -Z80f8ab0808064dfb run:vulkan_version

vkEnumerateInstanceVersion is showing up, so maybe the other two libvulkan.so.1 symbols aren’t showing up because Chimera splits the debug info into a separate file?

1 Like

The unwinding logic in zig can read separate debug files :slight_smile: But it depends on #25668. I don’t know if you are on a recent build.

As for me. I feel like I’m going crazy… I’m slowly convincing myself there’s a bug in glibc’s __memcpy_sse2_unaligned_erms, which is objectively impossible.

1 Like
❯ zig-nightly version
0.16.0-dev.1216+846854972

Checking out the zig git repo and checking out the merged commit gives gives a version of 0.15.0-1205-g62de7a2ef, so I should have the change.

zig on  HEAD (62de7a2) via C v21.1.4-clang via △ v4.1.2 via ↯ v0.15.2 
❯ jj new 62de7a2efd760bec85a41d52eca641fd61cd4a5d
Done importing changes from the underlying Git repo.
Working copy  (@) now at: xlplntmn 07fbe94b (empty) (no description set)
Parent commit (@-)      : opuxowrt 62de7a2e fix typo in std.debug.ElfFile.loadSeparateDebugFile

zig on  HEAD (62de7a2) via C v21.1.4-clang via △ v4.1.2 via ↯ v0.15.2 
❯ git -C . --git-dir .git describe --match '*.*.*' --tags --abbrev=9
0.15.0-1205-g62de7a2ef

It turns out I didn’t have the debug files installed. After installing the debug info for musl and libvulkan, it still didn’t work, because Zig was trying to load the debug info from /usr/lib/debug/lib64/libvulkan.so.1.326. Chimera only has the path at /usr/lib/debug/usr/lib/libvulkan.so.1.326.

I don’t know whether to call that a Zig bug or a Chimera Linux bug, though. /lib64 and /usr/lib64 are symlinks to /usr/lib/.

In any case, I added a symlink so Zig would find the debug info, and here’s the stack trace with symbols:

General protection exception (no address available)
???:?:?: 0x7ec0764576e5 in _mi_heap_get_free_small_page (../mimalloc/src/mimalloc.c)
Unwind error at address `/lib64/libc.so:0x7ec0764576e5` (unwind info unavailable), remaining frames may be incorrect
???:?:?: 0x7ec07680b66a in get_unix_settings_path (../loader/settings.c)
???:?:?: 0x7ec07680bff5 in update_global_loader_settings (../loader/settings.c)
???:?:?: 0x7ec07680ee45 in vkEnumerateInstanceVersion (../loader/trampoline.c)
/home/geemili/code/klaji/dynamic-loader/examples/vulkan_version.zig:20:39: 0x119deb3 in main (vulkan_version.zig)
    switch (vkEnumerateInstanceVersion(&vk_version)) {
                                      ^
/home/geemili/.local/share/ziglang/0.16.0-dev.1216+846854972/lib/std/start.zig:696:37: 0x119e5f3 in callMain (std.zig)
            const result = root.main() catch |err| {
                                    ^
/home/geemili/.local/share/ziglang/0.16.0-dev.1216+846854972/lib/std/start.zig:237:5: 0x11854e1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
run:vulkan_version
└─ run exe vulkan_version failure
error: process terminated unexpectedly
failed command: ./.zig-cache/o/05217588b239f06207545d11f928b000/vulkan_version

Build Summary: 1/3 steps succeeded (1 failed)
run:vulkan_version transitive failure
└─ run exe vulkan_version failure

error: the following build command failed with exit code 1:
.zig-cache/o/d975885aa5550873cf117d9404d8dbc8/build /home/geemili/.local/share/ziglang/0.16.0-dev.1216+846854972/zig /home/geemili/.local/share/ziglang/0.16.0-dev.1216+846854972/lib /home/geemili/code/klaji/dynamic-loader .zig-cache /home/geemili/.cache/zig --seed 0x47375e08 -Za503d9cb6bc8e1a8 run:vulkan_version
1 Like

issue on repo with debug log attached

I’m confused as to why the unwind info is unavailable, as both libraries have .eh_frame and .eh_frame_hdr sections:

❯ readelf --headers /usr/lib/libvulkan.so.1
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          524232 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         10
  Size of section headers:           64 (bytes)
  Number of section headers:         26
  Section header string table index: 24
There are 26 section headers, starting at offset 0x7ffc8:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .note.gnu.build-id NOTE           0000000000000270 000270 000024 00   A  0   0  4
  [ 2] .dynsym           DYNSYM          0000000000000298 000298 001ef0 18   A  4   1  8
  [ 3] .gnu.hash         GNU_HASH        0000000000002188 002188 000758 00   A  2   0  8
  [ 4] .dynstr           STRTAB          00000000000028e0 0028e0 001c26 00   A  0   0  1
  [ 5] .rela.dyn         RELA            0000000000004508 004508 001668 18   A  2   0  8
  [ 6] .relr.dyn         RELR            0000000000005b70 005b70 000088 08   A  0   0  8
  [ 7] .rela.plt         RELA            0000000000005bf8 005bf8 000558 18  AI  2  20  8
  [ 8] .rodata           PROGBITS        0000000000006150 006150 021944 00 AMS  0   0 16
  [ 9] .eh_frame_hdr     PROGBITS        0000000000027a94 027a94 001f1c 00   A  0   0  4
  [10] .eh_frame         PROGBITS        00000000000299b0 0299b0 0080e4 00   A  0   0  8
  [11] .text             PROGBITS        0000000000032aa0 031aa0 04b8ff 00  AX  0   0 16
  [12] .init             PROGBITS        000000000007e39f 07d39f 000003 00  AX  0   0  1
  [13] .fini             PROGBITS        000000000007e3a2 07d3a2 000003 00  AX  0   0  1
  [14] .plt              PROGBITS        000000000007e3b0 07d3b0 0003a0 00  AX  0   0 16
  [15] .init_array       INIT_ARRAY      000000000007f750 07d750 000010 00  WA  0   0  8
  [16] .fini_array       FINI_ARRAY      000000000007f760 07d760 000010 00  WA  0   0  8
  [17] .data.rel.ro      PROGBITS        000000000007f770 07d770 001c20 00  WA  0   0 16
  [18] .dynamic          DYNAMIC         0000000000081390 07f390 0001a0 10  WA  4   0  8
  [19] .got              PROGBITS        0000000000081530 07f530 000770 00  WA  0   0  8
  [20] .got.plt          PROGBITS        0000000000081ca0 07fca0 0001e0 00  WA  0   0  8
  [21] .relro_padding    NOBITS          0000000000081e80 07fe80 000180 00  WA  0   0  1
  [22] .data             PROGBITS        0000000000082e80 07fe80 00003b 00  WA  0   0  8
  [23] .bss              NOBITS          0000000000082ec0 07febb 000168 00  WA  0   0  8
  [24] .shstrtab         STRTAB          0000000000000000 07febb 0000ed 00      0   0  1
  [25] .gnu_debuglink    PROGBITS        0000000000000000 07ffa8 00001c 00      0   0  4
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  R (retain), l (large), p (processor specific)

Elf file type is DYN (Shared object file)
Entry point 0x0
There are 10 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x000230 0x000230 R   0x8
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x031a94 0x031a94 R   0x1000
  LOAD           0x031aa0 0x0000000000032aa0 0x0000000000032aa0 0x04bcb0 0x04bcb0 R E 0x1000
  LOAD           0x07d750 0x000000000007f750 0x000000000007f750 0x002730 0x0028b0 RW  0x1000
  LOAD           0x07fe80 0x0000000000082e80 0x0000000000082e80 0x00003b 0x0001a8 RW  0x1000
  DYNAMIC        0x07f390 0x0000000000081390 0x0000000000081390 0x0001a0 0x0001a0 RW  0x8
  GNU_RELRO      0x07d750 0x000000000007f750 0x000000000007f750 0x002730 0x0028b0 R   0x1
  GNU_EH_FRAME   0x027a94 0x0000000000027a94 0x0000000000027a94 0x001f1c 0x001f1c R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x0
  NOTE           0x000270 0x0000000000000270 0x0000000000000270 0x000024 0x000024 R   0x4

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .note.gnu.build-id .dynsym .gnu.hash .dynstr .rela.dyn .relr.dyn .rela.plt .rodata .eh_frame_hdr .eh_frame 
   02     .text .init .fini .plt 
   03     .init_array .fini_array .data.rel.ro .dynamic .got .got.plt .relro_padding 
   04     .data .bss 
   05     .dynamic 
   06     .init_array .fini_array .data.rel.ro .dynamic .got .got.plt .relro_padding 
   07     .eh_frame_hdr 
   08     
   09     .note.gnu.build-id 
   None   .shstrtab .gnu_debuglink 
❯ readelf --headers /usr/lib/libc.so
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0xC6878
  Start of program headers:          64 (bytes into file)
  Start of section headers:          847880 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         10
  Size of section headers:           64 (bytes)
  Number of section headers:         23
  Section header string table index: 21
There are 23 section headers, starting at offset 0xcf008:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .note.gnu.build-id NOTE           0000000000000270 000270 000024 00   A  0   0  4
  [ 2] .dynsym           DYNSYM          0000000000000298 000298 009ab0 18   A  5   1  8
  [ 3] .gnu.hash         GNU_HASH        0000000000009d48 009d48 003044 00   A  2   0  8
  [ 4] .hash             HASH            000000000000cd8c 00cd8c 003398 04   A  2   0  4
  [ 5] .dynstr           STRTAB          0000000000010124 010124 00432e 00   A  0   0  1
  [ 6] .rela.dyn         RELA            0000000000014458 014458 000180 18   A  2   0  8
  [ 7] .relr.dyn         RELR            00000000000145d8 0145d8 0000a8 08   A  0   0  8
  [ 8] .rela.plt         RELA            0000000000014680 014680 000090 18  AI  2  17  8
  [ 9] .rodata           PROGBITS        0000000000014740 014740 03681d 00 AMS  0   0 64
  [10] .eh_frame_hdr     PROGBITS        000000000004af60 04af60 000024 00   A  0   0  4
  [11] .eh_frame         PROGBITS        000000000004af88 04af88 00006c 00   A  0   0  8
  [12] .text             PROGBITS        000000000004c000 04b000 080c01 00  AX  0   0 16
  [13] .plt              PROGBITS        00000000000ccc10 0cbc10 000070 00  AX  0   0 16
  [14] .data.rel.ro      PROGBITS        00000000000cdc80 0cbc80 001678 00  WA  0   0 64
  [15] .dynamic          DYNAMIC         00000000000cf2f8 0cd2f8 000150 10  WA  5   0  8
  [16] .got              PROGBITS        00000000000cf448 0cd448 000080 00  WA  0   0  8
  [17] .got.plt          PROGBITS        00000000000cf4c8 0cd4c8 000048 00  WA  0   0  8
  [18] .relro_padding    NOBITS          00000000000cf510 0cd510 000af0 00  WA  0   0  1
  [19] .data             PROGBITS        00000000000d0510 0cd510 001a20 00  WA  0   0 16
  [20] .bss              NOBITS          00000000000d1f40 0cef30 0ad040 00  WA  0   0 64
  [21] .shstrtab         STRTAB          0000000000000000 0cef30 0000c9 00      0   0  1
  [22] .gnu_debuglink    PROGBITS        0000000000000000 0ceffc 00000c 00      0   0  4
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  R (retain), l (large), p (processor specific)

Elf file type is DYN (Shared object file)
Entry point 0xc6878
There are 10 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x000230 0x000230 R   0x8
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x04aff4 0x04aff4 R   0x1000
  LOAD           0x04b000 0x000000000004c000 0x000000000004c000 0x080c80 0x080c80 R E 0x1000
  LOAD           0x0cbc80 0x00000000000cdc80 0x00000000000cdc80 0x001890 0x002380 RW  0x1000
  LOAD           0x0cd510 0x00000000000d0510 0x00000000000d0510 0x001a20 0x0aea70 RW  0x1000
  DYNAMIC        0x0cd2f8 0x00000000000cf2f8 0x00000000000cf2f8 0x000150 0x000150 RW  0x8
  GNU_RELRO      0x0cbc80 0x00000000000cdc80 0x00000000000cdc80 0x001890 0x002380 R   0x1
  GNU_EH_FRAME   0x04af60 0x000000000004af60 0x000000000004af60 0x000024 0x000024 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x0
  NOTE           0x000270 0x0000000000000270 0x0000000000000270 0x000024 0x000024 R   0x4

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .note.gnu.build-id .dynsym .gnu.hash .hash .dynstr .rela.dyn .relr.dyn .rela.plt .rodata .eh_frame_hdr .eh_frame 
   02     .text .plt 
   03     .data.rel.ro .dynamic .got .got.plt .relro_padding 
   04     .data .bss 
   05     .dynamic 
   06     .data.rel.ro .dynamic .got .got.plt .relro_padding 
   07     .eh_frame_hdr 
   08     
   09     .note.gnu.build-id 
   None   .shstrtab .gnu_debuglink 

So I didn’t figure out why the mimalloc was crashing, but I was able to work around it by using vulkan’s API to provide allocation callbacks. I combined that with implementing a couple of libc functions, and now I’m getting an invalid free instead! Exciting progress :laughing:.

I also managed to crash the Zig compiler while trying to implement glibc’s rtld stapsdt probe debugger interface:

Anyway, I’m done working on this for today.

3 Likes

Exciting indeed :slight_smile: I agree that implementing libc functions is a good way to bypass the problematic libc / dynamic linker relation. But personally, I intend to keep substitutions as minimal as possible, ideally only ld stuff.

After a much-needed break to avoid definitive madness, I discovered a few things:

  • The __memcpy_sse2_unaligned_erms bug was caused by the fact that __x86_shared_non_temporal_threshold wasn’t initialized. It’s normally done by the init_cacheinfo function, which is called very early, as it is invoked by an IRELATIVE relocation resolver (weird technique). But this function depends on ld’s cpu_features. This implies that dependencies must be fully relocated before relocating a library, which was not the case.
  • I then had an issue with libc’s tolower, which revealed that __libc_tsd_address and friends were unusable. It uses TLS, and this was a clear indication that TLS handling was wrong. I thought it was OK because printf and malloc worked, but I think that was by pure accident. I discovered an embarrassingly obvious bug in mapTlsBlock, where I used the file bytes instead of the mapped ones, which made any .tdata modifications due to relocations overridden. Additionally, I think I must call __libc_early_init manually, like ld does, because ctype tables are initialized in it.
  • And after having corrected all that, malloc became unusable because of malloc(): unaligned tcache chunk detected :neutral_face:

I think the prototyping will take far more time than expected… I will update the gist later today, in case you are interested in porting the latest changes.

3 Likes