Compare commits

..

5 Commits

Author SHA1 Message Date
5825d12b97 simplify loading interpreter 2025-10-24 16:34:36 +02:00
8fb72a9928 use faller for logging 2025-10-24 15:01:17 +02:00
57f74fddf2 refactor tests 2025-10-20 10:46:22 +02:00
4382dda192 Merge pull request 'PATH lookup' (#1) from path_lookup into main
Reviewed-on: #1
2025-10-20 08:31:03 +00:00
87dbba3b9c PATH lookup 2025-10-20 10:29:32 +02:00
4 changed files with 126 additions and 119 deletions

View File

@@ -26,6 +26,8 @@ zig build
Alternatively use something like this to run directly: Alternatively use something like this to run directly:
```sh ```sh
zig build run -- /bin/ls zig build run -- /bin/ls
# or
zig build run -- ls
``` ```
This runs the tests: This runs the tests:
@@ -44,6 +46,10 @@ passed through to the target executable.
# Run a test executable that prints its arguments # Run a test executable that prints its arguments
./zig-out/bin/loader ./zig-out/bin/test_nolibc_pie_printArgs foo bar baz ./zig-out/bin/loader ./zig-out/bin/test_nolibc_pie_printArgs foo bar baz
# Output: ./zig-out/bin/test_nolibc_pie_printArgs foo bar baz # Output: ./zig-out/bin/test_nolibc_pie_printArgs foo bar baz
# Run echo
./zig-out/bin/loader echo Hello There
# Output: Hello There
``` ```
## License ## License

View File

@@ -42,17 +42,20 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
try compileTestApplications(b, target, optimize, false, false); const faller = b.dependency("faller", .{
try compileTestApplications(b, target, optimize, false, true); .target = target,
try compileTestApplications(b, target, optimize, true, true); .optimize = optimize,
});
const mod = b.addModule("loader", .{ const mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"), .root_source_file = b.path("src/main.zig"),
.optimize = optimize, .optimize = optimize,
.target = target, .target = target,
.link_libc = false, .link_libc = false,
.link_libcpp = false, .link_libcpp = false,
}); });
mod.addImport("faller", faller.module("faller"));
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "loader", .name = "loader",
.root_module = mod, .root_module = mod,
@@ -68,6 +71,10 @@ pub fn build(b: *std.Build) !void {
run_cmd.addArgs(args); run_cmd.addArgs(args);
} }
try compileTestApplications(b, target, optimize, false, false);
try compileTestApplications(b, target, optimize, false, true);
try compileTestApplications(b, target, optimize, true, true);
const exe_tests = b.addTest(.{ .root_module = mod }); const exe_tests = b.addTest(.{ .root_module = mod });
const run_exe_tests = b.addRunArtifact(exe_tests); const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests"); const test_step = b.step("test", "Run tests");

View File

@@ -1,6 +1,12 @@
.{ .{
.name = .loader, .name = .loader,
.version = "0.0.1", .version = "0.0.1",
.dependencies = .{
.faller = .{
.url = "git+https://git.pascalzittlau.de/pzittlau/faller.git#2dea92dcd9184c3a37f07761009944bc8fd4d352",
.hash = "faller-0.1.0-nNGNDT07AADmPJe_o0ERuMpZJGgXbdb_5B7VAMs2SmEt",
},
},
.minimum_zig_version = "0.15.1", .minimum_zig_version = "0.15.1",
.paths = .{""}, .paths = .{""},
.fingerprint = 0xbf53f276a39b2af9, .fingerprint = 0xbf53f276a39b2af9,

View File

@@ -1,3 +1,4 @@
const faller = @import("faller");
const std = @import("std"); const std = @import("std");
const elf = std.elf; const elf = std.elf;
@@ -5,10 +6,11 @@ const mem = std.mem;
const posix = std.posix; const posix = std.posix;
const testing = std.testing; const testing = std.testing;
const assert = std.debug.assert; pub const faller_options: faller.Options = .{ .tags_disabled = &.{.debug} };
const Logger = faller.Logger(&.{.loader});
const log = std.log.scoped(.loader); const assert = std.debug.assert;
pub const std_options = std.Options{ .log_level = .info }; const log = Logger.log;
const page_size = std.heap.pageSize(); const page_size = std.heap.pageSize();
const max_interp_path_length = 128; const max_interp_path_length = 128;
@@ -39,55 +41,43 @@ pub fn main() !void {
return; return;
} }
// TODO: maybe search for the file in PATH
// Map file into memory // Map file into memory
const file = try std.fs.cwd().openFile( const file = try lookupFile(mem.sliceTo(std.os.argv[arg_index], 0));
mem.sliceTo(std.os.argv[arg_index], 0), var file_buffer: [128]u8 = undefined;
.{ .mode = .read_only }, var file_reader = file.reader(&file_buffer);
); log(.info, "--- Loading executable: {s} ---", .{std.os.argv[arg_index]});
var buffer: [128]u8 = undefined;
var file_reader = file.reader(&buffer);
log.info("--- Loading executable: {s} ---", .{std.os.argv[arg_index]});
const ehdr = try elf.Header.read(&file_reader.interface); const ehdr = try elf.Header.read(&file_reader.interface);
const base = try loadStaticElf(ehdr, &file_reader); const base = try loadStaticElf(ehdr, &file_reader);
const entry = ehdr.entry + if (ehdr.type == .DYN) base else 0; const entry = ehdr.entry + if (ehdr.type == .DYN) base else 0;
log.info("Executable loaded: base=0x{x}, entry=0x{x}", .{ base, entry }); log(.info, "Executable loaded: base=0x{x}, entry=0x{x}", .{ base, entry });
// Check for dynamic linker // Check for dynamic linker
const maybe_interp: ?std.fs.File = interp: {
var phdrs = ehdr.iterateProgramHeaders(&file_reader);
while (try phdrs.next()) |phdr| {
if (phdr.p_type != elf.PT_INTERP) continue;
var interp_path: [max_interp_path_length]u8 = undefined;
try file_reader.seekTo(phdr.p_offset);
if (try file_reader.read(interp_path[0..phdr.p_filesz]) != phdr.p_filesz)
return UnfinishedReadError.UnfinishedRead;
assert(interp_path[phdr.p_filesz - 1] == 0); // Must be zero terminated
log.info("Found interpreter path: {s}", .{interp_path[0 .. phdr.p_filesz - 1]});
break :interp try std.fs.cwd().openFile(
interp_path[0 .. phdr.p_filesz - 1],
.{ .mode = .read_only },
);
}
break :interp null;
};
// We don't need the file anymore. But we reuse the buffer if we need to load the interpreter.
// Therefore deinit everything.
file_reader = undefined;
file.close();
var maybe_interp_base: ?usize = null; var maybe_interp_base: ?usize = null;
var maybe_interp_entry: ?usize = null; var maybe_interp_entry: ?usize = null;
if (maybe_interp) |interp| { var phdrs = ehdr.iterateProgramHeaders(&file_reader);
log.info("--- Loading interpreter ---", .{}); while (try phdrs.next()) |phdr| {
var interp_reader = interp.reader(&buffer); if (phdr.p_type != elf.PT_INTERP) continue;
var interp_path: [max_interp_path_length]u8 = undefined;
try file_reader.seekTo(phdr.p_offset);
if (try file_reader.read(interp_path[0..phdr.p_filesz]) != phdr.p_filesz)
return UnfinishedReadError.UnfinishedRead;
assert(interp_path[phdr.p_filesz - 1] == 0); // Must be zero terminated
log(.info, "Found interpreter path: {s}", .{interp_path[0 .. phdr.p_filesz - 1]});
const interp = try std.fs.cwd().openFile(
interp_path[0 .. phdr.p_filesz - 1],
.{ .mode = .read_only },
);
log(.info, "--- Loading interpreter ---", .{});
var interp_buffer: [128]u8 = undefined;
var interp_reader = interp.reader(&interp_buffer);
const interp_ehdr = try elf.Header.read(&interp_reader.interface); const interp_ehdr = try elf.Header.read(&interp_reader.interface);
assert(interp_ehdr.type == elf.ET.DYN); assert(interp_ehdr.type == elf.ET.DYN);
const interp_base = try loadStaticElf(interp_ehdr, &interp_reader); const interp_base = try loadStaticElf(interp_ehdr, &interp_reader);
maybe_interp_base = interp_base; maybe_interp_base = interp_base;
maybe_interp_entry = interp_ehdr.entry + if (interp_ehdr.type == .DYN) interp_base else 0; maybe_interp_entry = interp_ehdr.entry + if (interp_ehdr.type == .DYN) interp_base else 0;
log.info("Interpreter loaded: base=0x{x}, entry=0x{x}", .{ interp_base, maybe_interp_entry.? }); log(.info, "Interpreter loaded: base=0x{x}, entry=0x{x}", .{ interp_base, maybe_interp_entry.? });
interp.close(); interp.close();
} }
@@ -115,7 +105,8 @@ pub fn main() !void {
const dest_ptr = @as([*]u8, @ptrCast(std.os.argv.ptr)); const dest_ptr = @as([*]u8, @ptrCast(std.os.argv.ptr));
const src_ptr = @as([*]u8, @ptrCast(&std.os.argv[arg_index])); const src_ptr = @as([*]u8, @ptrCast(&std.os.argv[arg_index]));
const len = @intFromPtr(end_of_auxv) - @intFromPtr(src_ptr); const len = @intFromPtr(end_of_auxv) - @intFromPtr(src_ptr);
log.debug( log(
.debug,
"Copying stack from {*} to {*} with length 0x{x}", "Copying stack from {*} to {*} with length 0x{x}",
.{ src_ptr, dest_ptr, len }, .{ src_ptr, dest_ptr, len },
); );
@@ -126,10 +117,10 @@ pub fn main() !void {
// start of the stack. // start of the stack.
const argc: [*]usize = @as([*]usize, @ptrCast(@alignCast(&std.os.argv.ptr[0]))) - 1; const argc: [*]usize = @as([*]usize, @ptrCast(@alignCast(&std.os.argv.ptr[0]))) - 1;
argc[0] = std.os.argv.len - arg_index; argc[0] = std.os.argv.len - arg_index;
log.debug("new argc: {x}", .{argc[0]}); log(.debug, "new argc: {x}", .{argc[0]});
const final_entry = maybe_interp_entry orelse entry; const final_entry = maybe_interp_entry orelse entry;
log.info("Trampolining to final entry: 0x{x} with sp: {*}", .{ final_entry, argc }); log(.info, "Trampolining to final entry: 0x{x} with sp: {*}", .{ final_entry, argc });
trampoline(final_entry, argc); trampoline(final_entry, argc);
} }
@@ -155,15 +146,15 @@ fn loadStaticElf(ehdr: elf.Header, file_reader: *std.fs.File.Reader) !usize {
} }
minva = mem.alignBackward(usize, minva, page_size); minva = mem.alignBackward(usize, minva, page_size);
maxva = mem.alignForward(usize, maxva, page_size); maxva = mem.alignForward(usize, maxva, page_size);
log.debug("Calculated bounds: minva=0x{x}, maxva=0x{x}", .{ minva, maxva }); log(.debug, "Calculated bounds: minva=0x{x}, maxva=0x{x}", .{ minva, maxva });
break :bounds .{ minva, maxva }; break :bounds .{ minva, maxva };
}; };
// Check, that the needed memory region can be allocated as a whole. We do this // Check, that the needed memory region can be allocated as a whole. We do this
const dynamic = ehdr.type == elf.ET.DYN; const dynamic = ehdr.type == elf.ET.DYN;
log.debug("ELF type is {s}", .{if (dynamic) "DYN" else "EXEC (static)"}); log(.debug, "ELF type is {s}", .{if (dynamic) "DYN" else "EXEC (static)"});
const hint = if (dynamic) null else @as(?[*]align(page_size) u8, @ptrFromInt(minva)); const hint = if (dynamic) null else @as(?[*]align(page_size) u8, @ptrFromInt(minva));
log.debug("mmap pre-flight hint: {*}", .{hint}); log(.debug, "mmap pre-flight hint: {*}", .{hint});
const base = try posix.mmap( const base = try posix.mmap(
hint, hint,
maxva - minva, maxva - minva,
@@ -172,7 +163,7 @@ fn loadStaticElf(ehdr: elf.Header, file_reader: *std.fs.File.Reader) !usize {
-1, -1,
0, 0,
); );
log.debug("Pre-flight reservation successful at: {*}, size: 0x{x}", .{ base.ptr, base.len }); log(.debug, "Pre-flight reservation successful at: {*}, size: 0x{x}", .{ base.ptr, base.len });
posix.munmap(base); posix.munmap(base);
const flags = posix.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true, .FIXED = true }; const flags = posix.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true, .FIXED = true };
@@ -188,7 +179,8 @@ fn loadStaticElf(ehdr: elf.Header, file_reader: *std.fs.File.Reader) !usize {
var start = mem.alignBackward(usize, phdr.p_vaddr, page_size); var start = mem.alignBackward(usize, phdr.p_vaddr, page_size);
const base_for_dyn = if (dynamic) @intFromPtr(base.ptr) else 0; const base_for_dyn = if (dynamic) @intFromPtr(base.ptr) else 0;
start += base_for_dyn; start += base_for_dyn;
log.debug( log(
.debug,
" - phdr[{}]: mapping 0x{x} bytes at 0x{x} (vaddr=0x{x}, dyn_base=0x{x})", " - phdr[{}]: mapping 0x{x} bytes at 0x{x} (vaddr=0x{x}, dyn_base=0x{x})",
.{ phdr_idx, size, start, phdr.p_vaddr, base_for_dyn }, .{ phdr_idx, size, start, phdr.p_vaddr, base_for_dyn },
); );
@@ -208,7 +200,7 @@ fn loadStaticElf(ehdr: elf.Header, file_reader: *std.fs.File.Reader) !usize {
return UnfinishedReadError.UnfinishedRead; return UnfinishedReadError.UnfinishedRead;
try posix.mprotect(ptr, elfToMmapProt(phdr.p_flags)); try posix.mprotect(ptr, elfToMmapProt(phdr.p_flags));
} }
log.debug("loadElf returning base: 0x{x}", .{@intFromPtr(base.ptr)}); log(.debug, "loadElf returning base: 0x{x}", .{@intFromPtr(base.ptr)});
return @intFromPtr(base.ptr); return @intFromPtr(base.ptr);
} }
@@ -221,6 +213,32 @@ fn elfToMmapProt(elf_prot: u64) u32 {
return result; return result;
} }
/// Opens the file by either opening via a (absolute or relative) path or searching through `PATH`
/// for a file with the name.
fn lookupFile(path_or_name: []const u8) !std.fs.File {
// If filename contains a slash ("/"), then it is interpreted as a pathname.
if (std.mem.indexOfScalarPos(u8, path_or_name, 0, '/')) |_| {
const fd = try posix.open(path_or_name, .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0);
return .{ .handle = fd };
}
// If it has no slash we need to look it up in PATH.
if (posix.getenvZ("PATH")) |env_path| {
var paths = std.mem.tokenizeScalar(u8, env_path, ':');
while (paths.next()) |p| {
var dir = std.fs.openDirAbsolute(p, .{}) catch continue;
defer dir.close();
const fd = posix.openat(dir.fd, path_or_name, .{
.ACCMODE = .RDONLY,
.CLOEXEC = true,
}, 0) catch continue;
return .{ .handle = fd };
}
}
return error.FileNotFound;
}
/// This function performs the final jump into the loaded program (amd64) /// This function performs the final jump into the loaded program (amd64)
// TODO: support more architectures // TODO: support more architectures
fn trampoline(entry: usize, sp: [*]usize) noreturn { fn trampoline(entry: usize, sp: [*]usize) noreturn {
@@ -236,98 +254,68 @@ fn trampoline(entry: usize, sp: [*]usize) noreturn {
// TODO: make this be passed in from the build system // TODO: make this be passed in from the build system
const bin_path = "zig-out/bin/"; const bin_path = "zig-out/bin/";
fn getExePath(comptime name: []const u8) []const u8 { fn getTestExePath(comptime name: []const u8) []const u8 {
return bin_path ++ name; return bin_path ++ "test_" ++ name;
} }
const loader_path = getExePath("loader"); const loader_path = bin_path ++ "loader";
test "test_nolibc_nopie_exit" { test "nolibc_nopie_exit" {
try testExit("test_nolibc_nopie_exit"); try testHelper(&.{ loader_path, getTestExePath("nolibc_nopie_exit") }, "");
} }
test "test_nolibc_pie_exit" { test "nolibc_pie_exit" {
try testExit("test_nolibc_pie_exit"); try testHelper(&.{ loader_path, getTestExePath("nolibc_pie_exit") }, "");
} }
test "test_libc_pie_exit" { test "libc_pie_exit" {
try testExit("test_libc_pie_exit"); try testHelper(&.{ loader_path, getTestExePath("libc_pie_exit") }, "");
} }
test "test_nolibc_nopie_helloWorld" { test "nolibc_nopie_helloWorld" {
try testHelloWorld("test_nolibc_nopie_helloWorld"); try testHelper(&.{ loader_path, getTestExePath("nolibc_nopie_helloWorld") }, "Hello World!\n");
} }
test "test_nolibc_pie_helloWorld" { test "nolibc_pie_helloWorld" {
try testHelloWorld("test_nolibc_pie_helloWorld"); try testHelper(&.{ loader_path, getTestExePath("nolibc_pie_helloWorld") }, "Hello World!\n");
} }
test "test_libc_pie_helloWorld" { test "libc_pie_helloWorld" {
try testHelloWorld("test_libc_pie_helloWorld"); try testHelper(&.{ loader_path, getTestExePath("libc_pie_helloWorld") }, "Hello World!\n");
} }
test "test_nolibc_nopie_printArgs" { test "nolibc_nopie_printArgs" {
try testPrintArgs("test_nolibc_nopie_printArgs"); try testPrintArgs("nolibc_nopie_printArgs");
} }
test "test_nolibc_pie_printArgs" { test "nolibc_pie_printArgs" {
try testPrintArgs("test_nolibc_pie_printArgs"); try testPrintArgs("nolibc_pie_printArgs");
} }
test "test_libc_pie_printArgs" { test "libc_pie_printArgs" {
try testPrintArgs("test_libc_pie_printArgs"); try testPrintArgs("libc_pie_printArgs");
} }
fn testExit(comptime name: []const u8) !void { test "echo" {
const exe_path = comptime getExePath(name); try testHelper(&.{ "echo", "Hello", "There" }, "Hello There\n");
const argv = &.{ loader_path, exe_path };
log.info("Running test: {s}", .{name});
const result = try std.process.Child.run(.{
.allocator = testing.allocator,
.argv = argv,
});
defer testing.allocator.free(result.stdout);
defer testing.allocator.free(result.stderr);
errdefer std.log.err("term: {}", .{result.term});
errdefer std.log.err("stdout: {s}", .{result.stdout});
try testing.expectEqualStrings("", result.stdout);
try testing.expect(result.term == .Exited);
try testing.expectEqual(0, result.term.Exited);
}
fn testHelloWorld(comptime name: []const u8) !void {
const exe_path = comptime getExePath(name);
const argv = &.{ loader_path, exe_path };
log.info("Running test: {s}", .{name});
const result = try std.process.Child.run(.{
.allocator = testing.allocator,
.argv = argv,
});
defer testing.allocator.free(result.stdout);
defer testing.allocator.free(result.stderr);
errdefer std.log.err("term: {}", .{result.term});
errdefer std.log.err("stdout: {s}", .{result.stdout});
try testing.expectEqualStrings("Hello World!\n", result.stdout);
try testing.expect(result.term == .Exited);
try testing.expectEqual(0, result.term.Exited);
} }
fn testPrintArgs(comptime name: []const u8) !void { fn testPrintArgs(comptime name: []const u8) !void {
const exe_path = comptime getExePath(name); const exe_path = getTestExePath(name);
const loader_argv: []const []const u8 = &.{ loader_path, exe_path, "foo", "bar", "baz", "hi" }; const loader_argv: []const []const u8 = &.{ loader_path, exe_path, "foo", "bar", "baz hi" };
const target_argv = loader_argv[1..]; const target_argv = loader_argv[1..];
log.info("Running test: {s}", .{name}); const expected_stout = try mem.join(testing.allocator, " ", target_argv);
defer testing.allocator.free(expected_stout);
try testHelper(loader_argv, expected_stout);
}
fn testHelper(
argv: []const []const u8,
expected_stdout: []const u8,
) !void {
const result = try std.process.Child.run(.{ const result = try std.process.Child.run(.{
.allocator = testing.allocator, .allocator = testing.allocator,
.argv = loader_argv, .argv = argv,
}); });
defer testing.allocator.free(result.stdout); defer testing.allocator.free(result.stdout);
defer testing.allocator.free(result.stderr); defer testing.allocator.free(result.stderr);
errdefer std.log.err("term: {}", .{result.term}); errdefer log(.err, "term: {}", .{result.term});
errdefer std.log.err("stdout: {s}", .{result.stdout}); errdefer log(.err, "stdout: {s}", .{result.stdout});
const expected_stout = try mem.join(testing.allocator, " ", target_argv); try testing.expectEqualStrings(expected_stdout, result.stdout);
defer testing.allocator.free(expected_stout);
try testing.expectEqualStrings(expected_stout, result.stdout);
try testing.expect(result.term == .Exited); try testing.expect(result.term == .Exited);
try testing.expectEqual(0, result.term.Exited); try testing.expectEqual(0, result.term.Exited);
} }