const builtin = @import("builtin"); const native_arch = builtin.cpu.arch; const native_endian = native_arch.endian(); const std = @import("std"); const leb = std.leb; const OP = std.dwarf.OP; const abi = std.debug.Dwarf.abi; const mem = std.mem; const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Writer = std.io.Writer; /// Expressions can be evaluated in different contexts, each requiring its own set of inputs. /// Callers should specify all the fields relevant to their context. If a field is required /// by the expression and it isn't in the context, error.IncompleteExpressionContext is returned. pub const Context = struct { /// The dwarf format of the section this expression is in format: std.dwarf.Format = .@"32", /// If specified, any addresses will pass through before being accessed memory_accessor: ?*std.debug.MemoryAccessor = null, /// The compilation unit this expression relates to, if any compile_unit: ?*const std.debug.Dwarf.CompileUnit = null, /// When evaluating a user-presented expression, this is the address of the object being evaluated object_address: ?*const anyopaque = null, /// .debug_addr section debug_addr: ?[]const u8 = null, /// Thread context thread_context: ?*std.debug.ThreadContext = null, reg_context: ?abi.RegisterContext = null, /// Call frame address, if in a CFI context cfa: ?usize = null, /// This expression is a sub-expression from an OP.entry_value instruction entry_value_context: bool = false, }; pub const Options = struct { /// The address size of the target architecture addr_size: u8 = @sizeOf(usize), /// Endianness of the target architecture endian: std.builtin.Endian = native_endian, /// Restrict the stack machine to a subset of opcodes used in call frame instructions call_frame_context: bool = false, }; pub const RunError = error{ UnimplementedExpressionCall, UnimplementedOpcode, UnimplementedUserOpcode, UnimplementedTypedComparison, UnimplementedTypeConversion, UnknownExpressionOpcode, IncompleteExpressionContext, InvalidCFAOpcode, InvalidExpression, InvalidFrameBase, InvalidIntegralTypeSize, InvalidRegister, InvalidSubExpression, InvalidTypeLength, TruncatedIntegralType, OutOfMemory, EndOfStream, Overflow, DivisionByZero, } || abi.RegBytesError; /// A stack machine that can decode and run DWARF expressions. /// Expressions can be decoded for non-native address size and endianness, /// but can only be executed if the current target matches the configuration. pub fn StackMachine(comptime options: Options) type { const Address = switch (options.addr_size) { 2 => u16, 4 => u32, 8 => u64, else => @compileError("Unsupported address size of " ++ options.addr_size), }; const SignedAddress = switch (options.addr_size) { 2 => i16, 4 => i32, 8 => i64, else => @compileError("Unsupported address size of " ++ options.addr_size), }; return struct { stack: std.ArrayListUnmanaged(Value) = .empty, const Self = @This(); const Value = union(enum) { generic: Address, // Typed value with a maximum size of a register regval_type: struct { // Offset of DW_TAG_base_type DIE type_offset: Address, type_size: u8, value: Address, }, // Typed value specified directly in the instruction stream const_type: struct { // Offset of DW_TAG_base_type DIE type_offset: Address, // Backed by the instruction stream value_bytes: []const u8, }, fn asIntegral(self: Value) !Address { return switch (self) { .generic => |v| v, // TODO: For these two prongs, look up the type and assert it's integral? .regval_type => |regval_type| regval_type.value, .const_type => |const_type| { const value: u64 = switch (const_type.value_bytes.len) { 1 => mem.readInt(u8, const_type.value_bytes[0..1], native_endian), 2 => mem.readInt(u16, const_type.value_bytes[0..2], native_endian), 4 => mem.readInt(u32, const_type.value_bytes[0..4], native_endian), 8 => mem.readInt(u64, const_type.value_bytes[0..8], native_endian), else => return error.InvalidIntegralTypeSize, }; return std.math.cast(Address, value) orelse error.TruncatedIntegralType; }, }; } fn fromInt(int: anytype) Value { const info = @typeInfo(@TypeOf(int)).int; if (@sizeOf(@TypeOf(int)) > options.addr_size) { return .{ .generic = switch (info.signedness) { .signed => @bitCast(@as(SignedAddress, @truncate(int))), .unsigned => @truncate(int), } }; } else { return .{ .generic = switch (info.signedness) { .signed => @bitCast(@as(SignedAddress, @intCast(int))), .unsigned => @intCast(int), } }; } } }; pub fn reset(self: *Self) void { self.stack.clearRetainingCapacity(); } pub fn deinit(self: *Self, allocator: Allocator) void { self.stack.deinit(allocator); } pub fn run( self: *Self, expression: []const u8, gpa: Allocator, context: Context, initial_value: ?usize, ) RunError!?Value { if (@sizeOf(usize) != @sizeOf(Address) or options.endian != native_endian) { // This restriction can be removed when the `@ptrFromInt` calls are removed. @compileError("Execution of non-native address sizes / endianness is not supported"); } const stack = &self.stack; if (initial_value) |i| try stack.append(gpa, .{ .generic = i }); var i: usize = 0; // TODO: https://github.com/ziglang/zig/issues/15556 op: switch (nextOpcode(expression, &i)) { // 2.5.1.1: Literal Encodings @intFromEnum(OP.lit0)...@intFromEnum(OP.lit31) => |n| { try stack.append(gpa, .{ .generic = n - @intFromEnum(OP.lit0) }); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.addr) => { try stack.append(gpa, .fromInt(try nextInt(expression, &i, Address))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.const1u) => { try stack.append(gpa, .fromInt(try nextInt(expression, &i, u8))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.const2u) => { try stack.append(gpa, .fromInt(try nextInt(expression, &i, u16))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.const4u) => { try stack.append(gpa, .fromInt(try nextInt(expression, &i, u32))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.const8u) => { try stack.append(gpa, .fromInt(try nextInt(expression, &i, u64))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.const1s) => { try stack.append(gpa, .fromInt(try nextInt(expression, &i, i8))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.const2s) => { try stack.append(gpa, .fromInt(try nextInt(expression, &i, i16))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.const4s) => { try stack.append(gpa, .fromInt(try nextInt(expression, &i, i32))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.const8s) => { try stack.append(gpa, .fromInt(try nextInt(expression, &i, i64))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.constu) => { try stack.append(gpa, .fromInt(try nextLeb128(expression, &i, u64))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.consts) => { try stack.append(gpa, .fromInt(try nextLeb128(expression, &i, i64))); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.const_type) => { if (options.call_frame_context) return error.InvalidCFAOpcode; try stack.append(gpa, .{ .const_type = .{ .type_offset = try nextLeb128(expression, &i, Address), .value_bytes = try nextSlice(expression, &i, try nextInt(expression, &i, u8)), } }); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.addrx), @intFromEnum(OP.constx), => { if (options.call_frame_context) return error.InvalidCFAOpcode; const compile_unit = context.compile_unit orelse return error.IncompleteExpressionContext; const debug_addr = context.debug_addr orelse return error.IncompleteExpressionContext; const debug_addr_index = try nextLeb128(expression, &i, u64); const offset = compile_unit.addr_base + debug_addr_index; if (offset >= debug_addr.len) return error.InvalidExpression; const value = mem.readInt(Address, debug_addr[offset..][0..@sizeOf(Address)], options.endian); try stack.append(gpa, .fromInt(value)); continue :op nextOpcode(expression, &i); }, // 2.5.1.2: Register Values @intFromEnum(OP.fbreg) => { const compile_unit = context.compile_unit orelse return error.IncompleteExpressionContext; const frame_base = compile_unit.frame_base orelse return error.IncompleteExpressionContext; const offset = try nextLeb128(expression, &i, i64); _ = offset; switch (frame_base.*) { .exprloc => { // TODO: Run this expression in a nested stack machine return error.UnimplementedOpcode; }, .loclistx => { // TODO: Read value from .debug_loclists return error.UnimplementedOpcode; }, .sec_offset => { // TODO: Read value from .debug_loclists return error.UnimplementedOpcode; }, else => return error.InvalidFrameBase, } }, @intFromEnum(OP.breg0)...@intFromEnum(OP.breg31) => |n| { const thread_context = context.thread_context orelse return error.IncompleteExpressionContext; const base_register = n - @intFromEnum(OP.breg0); const offset = try nextLeb128(expression, &i, i64); const reg_bytes = try abi.regBytes(thread_context, base_register, context.reg_context); const start_addr = mem.readInt(Address, reg_bytes[0..@sizeOf(Address)], options.endian); try stack.append(gpa, .{ .generic = std.math.addAny(Address, start_addr, offset) orelse return error.InvalidExpression, }); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.bregx) => { const thread_context = context.thread_context orelse return error.IncompleteExpressionContext; const base_register = try nextLeb128(expression, &i, u8); const offset = try nextLeb128(expression, &i, i64); const reg_bytes = try abi.regBytes(thread_context, base_register, context.reg_context); const start_addr = mem.readInt(Address, reg_bytes[0..@sizeOf(Address)], options.endian); try stack.append(gpa, .{ .generic = std.math.addAny(Address, start_addr, offset) orelse return error.InvalidExpression, }); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.regval_type) => { if (options.call_frame_context) return error.InvalidCFAOpcode; const thread_context = context.thread_context orelse return error.IncompleteExpressionContext; const register = try nextLeb128(expression, &i, u8); const type_offset = try nextLeb128(expression, &i, Address); const reg_bytes = try abi.regBytes(thread_context, register, context.reg_context); const value = mem.readInt(Address, reg_bytes[0..@sizeOf(Address)], options.endian); try stack.append(gpa, .{ .regval_type = .{ .type_offset = type_offset, .type_size = @sizeOf(Address), .value = value, }, }); continue :op nextOpcode(expression, &i); }, // 2.5.1.3: Stack Operations @intFromEnum(OP.dup) => { if (stack.items.len == 0) return error.InvalidExpression; try stack.append(gpa, stack.items[stack.items.len - 1]); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.drop) => { _ = stack.pop(); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.pick) => { const stack_index = try nextInt(expression, &i, u8); if (stack_index >= stack.items.len) return error.InvalidExpression; try stack.append(gpa, stack.items[stack.items.len - 1 - stack_index]); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.over) => { if (stack.items.len < 2) return error.InvalidExpression; try stack.append(gpa, stack.items[stack.items.len - 2]); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.swap) => { if (stack.items.len < 2) return error.InvalidExpression; mem.swap(Value, &stack.items[stack.items.len - 1], &stack.items[stack.items.len - 2]); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.rot) => { if (stack.items.len < 3) return error.InvalidExpression; const first = stack.items[stack.items.len - 1]; stack.items[stack.items.len - 1] = stack.items[stack.items.len - 2]; stack.items[stack.items.len - 2] = stack.items[stack.items.len - 3]; stack.items[stack.items.len - 3] = first; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.deref_type) => { if (options.call_frame_context) return error.InvalidCFAOpcode; if (stack.items.len == 0) return error.InvalidExpression; const size = try nextInt(expression, &i, u8); const type_offset = try nextLeb128(expression, &i, Address); const addr = try stack.items[stack.items.len - 1].asIntegral(); const loaded = try accessAddress(size, addr, context.memory_accessor); stack.items[stack.items.len - 1] = .{ .regval_type = .{ .type_offset = type_offset, .type_size = size, .value = loaded, }, }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.deref_size) => { if (stack.items.len == 0) return error.InvalidExpression; const addr = try stack.items[stack.items.len - 1].asIntegral(); const type_size = try nextInt(expression, &i, u8); const loaded = try accessAddress(type_size, addr, context.memory_accessor); stack.items[stack.items.len - 1] = .{ .generic = loaded }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.xderef_size) => { if (stack.items.len < 2) return error.InvalidExpression; const type_size = try nextInt(expression, &i, u8); const addr = try stack.pop().?.asIntegral(); const addr_space_identifier = try stack.items[stack.items.len - 1].asIntegral(); // Usage of addr_space_identifier in the address calculation is implementation defined. // This code will need to be updated to handle any architectures that utilize this. _ = addr_space_identifier; const loaded = try accessAddress(type_size, addr, context.memory_accessor); stack.items[stack.items.len - 1] = .{ .generic = loaded }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.deref) => { if (stack.items.len == 0) return error.InvalidExpression; const addr = try stack.items[stack.items.len - 1].asIntegral(); const loaded = try accessAddress(@sizeOf(Address), addr, context.memory_accessor); stack.items[stack.items.len - 1] = .{ .generic = loaded }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.xderef) => { if (stack.items.len < 2) return error.InvalidExpression; const addr = try stack.pop().?.asIntegral(); const addr_space_identifier = try stack.items[stack.items.len - 1].asIntegral(); // Usage of addr_space_identifier in the address calculation is implementation defined. // This code will need to be updated to handle any architectures that utilize this. _ = addr_space_identifier; const loaded = try accessAddress(@sizeOf(Address), addr, context.memory_accessor); stack.items[stack.items.len - 1] = .{ .generic = loaded }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.xderef_type), => { if (stack.items.len < 2) return error.InvalidExpression; const addr = try stack.pop().?.asIntegral(); const addr_space_identifier = try stack.items[stack.items.len - 1].asIntegral(); // Usage of addr_space_identifier in the address calculation is implementation defined. // This code will need to be updated to handle any architectures that utilize this. _ = addr_space_identifier; const size = try nextInt(expression, &i, u8); const type_offset = try nextLeb128(expression, &i, Address); const loaded = try accessAddress(size, addr, context.memory_accessor); stack.items[stack.items.len - 1] = .{ .regval_type = .{ .type_offset = type_offset, .type_size = size, .value = loaded, }, }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.push_object_address) => { if (options.call_frame_context) return error.InvalidCFAOpcode; // In sub-expressions, `push_object_address` is not meaningful (as per the // spec), so treat it like a nop if (!context.entry_value_context) { if (context.object_address == null) return error.IncompleteExpressionContext; try stack.append(gpa, .{ .generic = @intFromPtr(context.object_address.?) }); } }, @intFromEnum(OP.form_tls_address) => { return error.UnimplementedOpcode; }, @intFromEnum(OP.call_frame_cfa) => { if (options.call_frame_context) return error.InvalidCFAOpcode; if (context.cfa) |cfa| { try stack.append(gpa, .{ .generic = cfa }); } else return error.IncompleteExpressionContext; continue :op nextOpcode(expression, &i); }, // 2.5.1.4: Arithmetic and Logical Operations @intFromEnum(OP.abs) => { if (stack.items.len == 0) return error.InvalidExpression; const value: isize = @bitCast(try stack.items[stack.items.len - 1].asIntegral()); stack.items[stack.items.len - 1] = .{ .generic = @abs(value), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.@"and") => { if (stack.items.len < 2) return error.InvalidExpression; const a = try stack.pop().?.asIntegral(); stack.items[stack.items.len - 1] = .{ .generic = a & try stack.items[stack.items.len - 1].asIntegral(), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.div) => { if (stack.items.len < 2) return error.InvalidExpression; const a: isize = @bitCast(try stack.pop().?.asIntegral()); const b: isize = @bitCast(try stack.items[stack.items.len - 1].asIntegral()); stack.items[stack.items.len - 1] = .{ .generic = @bitCast(try std.math.divTrunc(isize, b, a)), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.minus) => { if (stack.items.len < 2) return error.InvalidExpression; const b = try stack.pop().?.asIntegral(); stack.items[stack.items.len - 1] = .{ .generic = try std.math.sub(Address, try stack.items[stack.items.len - 1].asIntegral(), b), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.mod) => { if (stack.items.len < 2) return error.InvalidExpression; const a: isize = @bitCast(try stack.pop().?.asIntegral()); const b: isize = @bitCast(try stack.items[stack.items.len - 1].asIntegral()); stack.items[stack.items.len - 1] = .{ .generic = @bitCast(@mod(b, a)), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.mul) => { if (stack.items.len < 2) return error.InvalidExpression; const a: isize = @bitCast(try stack.pop().?.asIntegral()); const b: isize = @bitCast(try stack.items[stack.items.len - 1].asIntegral()); stack.items[stack.items.len - 1] = .{ .generic = @bitCast(@mulWithOverflow(a, b)[0]), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.neg) => { if (stack.items.len == 0) return error.InvalidExpression; stack.items[stack.items.len - 1] = .{ .generic = @bitCast( try std.math.negate( @as(isize, @bitCast(try stack.items[stack.items.len - 1].asIntegral())), ), ), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.not) => { if (stack.items.len == 0) return error.InvalidExpression; stack.items[stack.items.len - 1] = .{ .generic = ~try stack.items[stack.items.len - 1].asIntegral(), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.@"or") => { if (stack.items.len < 2) return error.InvalidExpression; const a = try stack.pop().?.asIntegral(); stack.items[stack.items.len - 1] = .{ .generic = a | try stack.items[stack.items.len - 1].asIntegral(), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.plus) => { if (stack.items.len < 2) return error.InvalidExpression; const b = try stack.pop().?.asIntegral(); stack.items[stack.items.len - 1] = .{ .generic = try std.math.add(Address, try stack.items[stack.items.len - 1].asIntegral(), b), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.plus_uconst) => { if (stack.items.len == 0) return error.InvalidExpression; stack.items[stack.items.len - 1] = .{ .generic = std.math.addAny( Address, try nextLeb128(expression, &i, u64), try stack.items[stack.items.len - 1].asIntegral(), ) orelse return error.Overflow }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.shl) => { if (stack.items.len < 2) return error.InvalidExpression; const a = try stack.pop().?.asIntegral(); const b = try stack.items[stack.items.len - 1].asIntegral(); stack.items[stack.items.len - 1] = .{ .generic = std.math.shl(usize, b, a), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.shr) => { if (stack.items.len < 2) return error.InvalidExpression; const a = try stack.pop().?.asIntegral(); const b = try stack.items[stack.items.len - 1].asIntegral(); stack.items[stack.items.len - 1] = .{ .generic = std.math.shr(usize, b, a), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.shra) => { if (stack.items.len < 2) return error.InvalidExpression; const a = try stack.pop().?.asIntegral(); const b: isize = @bitCast(try stack.items[stack.items.len - 1].asIntegral()); stack.items[stack.items.len - 1] = .{ .generic = @bitCast(std.math.shr(isize, b, a)), }; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.xor) => { if (stack.items.len < 2) return error.InvalidExpression; const a = try stack.pop().?.asIntegral(); stack.items[stack.items.len - 1] = .{ .generic = a ^ try stack.items[stack.items.len - 1].asIntegral(), }; continue :op nextOpcode(expression, &i); }, // 2.5.1.5: Control Flow Operations @intFromEnum(OP.le) => { try cmpOp(stack, .lte); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.ge) => { try cmpOp(stack, .gte); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.eq) => { try cmpOp(stack, .eq); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.lt) => { try cmpOp(stack, .lt); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.gt) => { try cmpOp(stack, .gt); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.ne) => { try cmpOp(stack, .neq); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.skip) => { const branch_offset = try nextInt(expression, &i, i16); i = std.math.addAny(usize, i, branch_offset) orelse return error.InvalidExpression; continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.bra) => { const branch_offset = try nextInt(expression, &i, i16); const condition = try (stack.pop() orelse return error.InvalidExpression).asIntegral(); if (condition != 0) { i = std.math.addAny(usize, i, branch_offset) orelse return error.InvalidExpression; } continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.call2) => { if (options.call_frame_context) return error.InvalidCFAOpcode; const debug_info_offset = nextInt(expression, &i, u16); _ = debug_info_offset; // TODO: Load a DIE entry at debug_info_offset in a .debug_info section (the spec says that it // can be in a separate exe / shared object from the one containing this expression). // Transfer control to the DW_AT_location attribute, with the current stack as input. return error.UnimplementedExpressionCall; }, @intFromEnum(OP.call4) => { if (options.call_frame_context) return error.InvalidCFAOpcode; const debug_info_offset = nextInt(expression, &i, u32); _ = debug_info_offset; // TODO: Load a DIE entry at debug_info_offset in a .debug_info section (the spec says that it // can be in a separate exe / shared object from the one containing this expression). // Transfer control to the DW_AT_location attribute, with the current stack as input. return error.UnimplementedExpressionCall; }, @intFromEnum(OP.call_ref) => { if (options.call_frame_context) return error.InvalidCFAOpcode; const debug_info_offset: u64 = switch (context.format) { .@"32" => nextInt(expression, &i, u32), .@"64" => nextInt(expression, &i, u64), }; _ = debug_info_offset; // TODO: Load a DIE entry at debug_info_offset in a .debug_info section (the spec says that it // can be in a separate exe / shared object from the one containing this expression). // Transfer control to the DW_AT_location attribute, with the current stack as input. return error.UnimplementedExpressionCall; }, // 2.5.1.6: Type Conversions @intFromEnum(OP.convert) => { if (options.call_frame_context) return error.InvalidCFAOpcode; if (stack.items.len == 0) return error.InvalidExpression; const type_offset = try nextLeb128(expression, &i, u64); // TODO: Load the DW_TAG_base_type entries in context.compile_unit and verify both types are the same size const value = stack.items[stack.items.len - 1]; if (type_offset == 0) { stack.items[stack.items.len - 1] = .{ .generic = try value.asIntegral() }; } else { // TODO: Load the DW_TAG_base_type entry in context.compile_unit, find a conversion operator // from the old type to the new type, run it. return error.UnimplementedTypeConversion; } continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.reinterpret) => { if (options.call_frame_context) return error.InvalidCFAOpcode; if (stack.items.len == 0) return error.InvalidExpression; const type_offset = try nextLeb128(expression, &i, u64); // TODO: Load the DW_TAG_base_type entries in context.compile_unit and verify both types are the same size const value = stack.items[stack.items.len - 1]; if (type_offset == 0) { stack.items[stack.items.len - 1] = .{ .generic = try value.asIntegral() }; } else { stack.items[stack.items.len - 1] = switch (value) { .generic => |v| .{ .regval_type = .{ .type_offset = type_offset, .type_size = @sizeOf(Address), .value = v, }, }, .regval_type => |r| .{ .regval_type = .{ .type_offset = type_offset, .type_size = r.type_size, .value = r.value, }, }, .const_type => |c| .{ .const_type = .{ .type_offset = type_offset, .value_bytes = c.value_bytes, }, }, }; } continue :op nextOpcode(expression, &i); }, // 2.5.1.7: Special Operations @intFromEnum(OP.nop) => continue :op nextOpcode(expression, &i), @intFromEnum(OP.entry_value) => { const block_len = try nextLeb128(expression, &i, usize); if (block_len == 0) return error.InvalidSubExpression; const block = try nextSlice(expression, &i, block_len); // TODO: The spec states that this sub-expression needs to observe the state (ie. registers) // as it was upon entering the current subprogram. If this isn't being called at the // end of a frame unwind operation, an additional ThreadContext with this state will be needed. switch (block[0]) { @intFromEnum(OP.reg0)...@intFromEnum(OP.reg31) => |n| { const thread_context = context.thread_context orelse return error.IncompleteExpressionContext; const register = n - @intFromEnum(OP.reg0); const reg_bytes = try abi.regBytes(thread_context, register, context.reg_context); const value = mem.readInt(usize, reg_bytes[0..@sizeOf(usize)], options.endian); try stack.append(gpa, .{ .generic = value }); continue :op nextOpcode(expression, &i); }, @intFromEnum(OP.regx) => { const thread_context = context.thread_context orelse return error.IncompleteExpressionContext; const register = try nextLeb128(expression, &i, u8); const reg_bytes = try abi.regBytes(thread_context, register, context.reg_context); const value = mem.readInt(usize, reg_bytes[0..@sizeOf(usize)], options.endian); try stack.append(gpa, .{ .generic = value }); continue :op nextOpcode(expression, &i); }, else => { var stack_machine: Self = .{}; defer stack_machine.deinit(gpa); var sub_context = context; sub_context.entry_value_context = true; const result = try stack_machine.run(block, gpa, sub_context, null); try stack.append(gpa, result orelse return error.InvalidSubExpression); continue :op nextOpcode(expression, &i); }, } }, @intFromEnum(OP.lo_user)...@intFromEnum(OP.hi_user) - 1 => return error.UnimplementedUserOpcode, // Repurposed for exiting the loop. @intFromEnum(OP.hi_user) => { if (stack.items.len == 0) return null; return stack.items[stack.items.len - 1]; }, else => { //std.debug.print("Unknown DWARF expression opcode: {x}\n", .{opcode}); return error.UnknownExpressionOpcode; }, } comptime unreachable; } fn nextOpcode(expression: []const u8, i: *usize) u8 { const index = i.*; if (expression.len - index == 0) return @intFromEnum(OP.hi_user); // repurposed to indicate end i.* = index + 1; return expression[index]; } fn nextInt(expression: []const u8, i: *usize, comptime I: type) !I { const n = @divExact(@bitSizeOf(I), 8); const slice = try nextSlice(expression, i, n); return mem.readInt(I, slice[0..n], options.endian); } fn nextSlice(expression: []const u8, i: *usize, len: usize) ![]const u8 { const index = i.*; if (expression.len - index < len) return error.EndOfStream; i.* = index + len; return expression[index..][0..len]; } fn nextLeb128(expression: []const u8, i: *usize, comptime I: type) !I { var br: std.io.Reader = .fixed(expression); br.seek = i.*; assert(br.seek <= br.end); const result = br.takeLeb128(I) catch |err| switch (err) { error.ReadFailed => unreachable, else => |e| return e, }; i.* = br.seek; return result; } fn accessAddress(size: u8, addr: Address, accessor: ?*std.debug.MemoryAccessor) !Address { if (accessor) |memory_accessor| { switch (size) { 1 => if (memory_accessor.load(u8, addr) == null) return error.InvalidExpression, 2 => if (memory_accessor.load(u16, addr) == null) return error.InvalidExpression, 4 => if (memory_accessor.load(u32, addr) == null) return error.InvalidExpression, 8 => if (memory_accessor.load(u64, addr) == null) return error.InvalidExpression, else => return error.InvalidExpression, } } return switch (size) { 1 => std.math.cast(Address, @as(*const u8, @ptrFromInt(addr)).*), 2 => std.math.cast(Address, @as(*const u16, @ptrFromInt(addr)).*), 4 => std.math.cast(Address, @as(*const u32, @ptrFromInt(addr)).*), 8 => std.math.cast(Address, @as(*const u64, @ptrFromInt(addr)).*), else => return error.InvalidExpression, } orelse return error.InvalidExpression; } fn cmpOp(stack: *std.ArrayListUnmanaged(Value), op: std.math.CompareOperator) !void { if (stack.items.len < 2) return error.InvalidExpression; const a = stack.pop().?; const b = stack.items[stack.items.len - 1]; const a_int = try a.asIntegral(); const b_int = try b.asIntegral(); stack.items[stack.items.len - 1] = .{ .generic = @intFromBool(std.math.compare(a_int, op, b_int)) }; } }; } pub fn Builder(comptime options: Options) type { const Address = switch (options.addr_size) { 2 => u16, 4 => u32, 8 => u64, else => @compileError("Unsupported address size of " ++ options.addr_size), }; return struct { /// Zero-operand instructions pub fn writeOpcode(writer: *Writer, comptime opcode: u8) !void { if (options.call_frame_context and !comptime isOpcodeValidInCFA(opcode)) return error.InvalidCFAOpcode; switch (opcode) { OP.dup, OP.drop, OP.over, OP.swap, OP.rot, OP.deref, OP.xderef, OP.push_object_address, OP.form_tls_address, OP.call_frame_cfa, OP.abs, OP.@"and", OP.div, OP.minus, OP.mod, OP.mul, OP.neg, OP.not, OP.@"or", OP.plus, OP.shl, OP.shr, OP.shra, OP.xor, OP.le, OP.ge, OP.eq, OP.lt, OP.gt, OP.ne, OP.nop, OP.stack_value, => try writer.writeByte(opcode), else => @compileError("This opcode requires operands, use `write()` instead"), } } // 2.5.1.1: Literal Encodings pub fn writeLiteral(writer: *Writer, literal: u8) !void { switch (literal) { 0...31 => |n| try writer.writeByte(n + OP.lit0), else => return error.InvalidLiteral, } } pub fn writeConst(writer: *Writer, comptime T: type, value: T) !void { if (@typeInfo(T) != .int) @compileError("Constants must be integers"); switch (T) { u8, i8, u16, i16, u32, i32, u64, i64 => { try writer.writeByte(switch (T) { u8 => OP.const1u, i8 => OP.const1s, u16 => OP.const2u, i16 => OP.const2s, u32 => OP.const4u, i32 => OP.const4s, u64 => OP.const8u, i64 => OP.const8s, else => unreachable, }); try writer.writeInt(T, value, options.endian); }, else => switch (@typeInfo(T).int.signedness) { .unsigned => { try writer.writeByte(OP.constu); try leb.writeUleb128(writer, value); }, .signed => { try writer.writeByte(OP.consts); try leb.writeIleb128(writer, value); }, }, } } pub fn writeConstx(writer: *Writer, debug_addr_offset: anytype) !void { try writer.writeByte(OP.constx); try leb.writeUleb128(writer, debug_addr_offset); } pub fn writeConstType(writer: *Writer, die_offset: anytype, value_bytes: []const u8) !void { if (options.call_frame_context) return error.InvalidCFAOpcode; if (value_bytes.len > 0xff) return error.InvalidTypeLength; try writer.writeByte(OP.const_type); try leb.writeUleb128(writer, die_offset); try writer.writeByte(@intCast(value_bytes.len)); try writer.writeAll(value_bytes); } pub fn writeAddr(writer: *Writer, value: Address) !void { try writer.writeByte(OP.addr); try writer.writeInt(Address, value, options.endian); } pub fn writeAddrx(writer: *Writer, debug_addr_offset: anytype) !void { if (options.call_frame_context) return error.InvalidCFAOpcode; try writer.writeByte(OP.addrx); try leb.writeUleb128(writer, debug_addr_offset); } // 2.5.1.2: Register Values pub fn writeFbreg(writer: *Writer, offset: anytype) !void { try writer.writeByte(OP.fbreg); try leb.writeIleb128(writer, offset); } pub fn writeBreg(writer: *Writer, register: u8, offset: anytype) !void { if (register > 31) return error.InvalidRegister; try writer.writeByte(OP.breg0 + register); try leb.writeIleb128(writer, offset); } pub fn writeBregx(writer: *Writer, register: anytype, offset: anytype) !void { try writer.writeByte(OP.bregx); try leb.writeUleb128(writer, register); try leb.writeIleb128(writer, offset); } pub fn writeRegvalType(writer: *Writer, register: anytype, offset: anytype) !void { if (options.call_frame_context) return error.InvalidCFAOpcode; try writer.writeByte(OP.regval_type); try leb.writeUleb128(writer, register); try leb.writeUleb128(writer, offset); } // 2.5.1.3: Stack Operations pub fn writePick(writer: *Writer, index: u8) !void { try writer.writeByte(OP.pick); try writer.writeByte(index); } pub fn writeDerefSize(writer: *Writer, size: u8) !void { try writer.writeByte(OP.deref_size); try writer.writeByte(size); } pub fn writeXDerefSize(writer: *Writer, size: u8) !void { try writer.writeByte(OP.xderef_size); try writer.writeByte(size); } pub fn writeDerefType(writer: *Writer, size: u8, die_offset: anytype) !void { if (options.call_frame_context) return error.InvalidCFAOpcode; try writer.writeByte(OP.deref_type); try writer.writeByte(size); try leb.writeUleb128(writer, die_offset); } pub fn writeXDerefType(writer: *Writer, size: u8, die_offset: anytype) !void { try writer.writeByte(OP.xderef_type); try writer.writeByte(size); try leb.writeUleb128(writer, die_offset); } // 2.5.1.4: Arithmetic and Logical Operations pub fn writePlusUconst(writer: *Writer, uint_value: anytype) !void { try writer.writeByte(OP.plus_uconst); try leb.writeUleb128(writer, uint_value); } // 2.5.1.5: Control Flow Operations pub fn writeSkip(writer: *Writer, offset: i16) !void { try writer.writeByte(OP.skip); try writer.writeInt(i16, offset, options.endian); } pub fn writeBra(writer: *Writer, offset: i16) !void { try writer.writeByte(OP.bra); try writer.writeInt(i16, offset, options.endian); } pub fn writeCall(writer: *Writer, comptime T: type, offset: T) !void { if (options.call_frame_context) return error.InvalidCFAOpcode; switch (T) { u16 => try writer.writeByte(OP.call2), u32 => try writer.writeByte(OP.call4), else => @compileError("Call operand must be a 2 or 4 byte offset"), } try writer.writeInt(T, offset, options.endian); } pub fn writeCallRef(writer: *Writer, comptime is_64: bool, value: if (is_64) u64 else u32) !void { if (options.call_frame_context) return error.InvalidCFAOpcode; try writer.writeByte(OP.call_ref); try writer.writeInt(if (is_64) u64 else u32, value, options.endian); } pub fn writeConvert(writer: *Writer, die_offset: anytype) !void { if (options.call_frame_context) return error.InvalidCFAOpcode; try writer.writeByte(OP.convert); try leb.writeUleb128(writer, die_offset); } pub fn writeReinterpret(writer: *Writer, die_offset: anytype) !void { if (options.call_frame_context) return error.InvalidCFAOpcode; try writer.writeByte(OP.reinterpret); try leb.writeUleb128(writer, die_offset); } // 2.5.1.7: Special Operations pub fn writeEntryValue(writer: *Writer, expression: []const u8) !void { try writer.writeByte(OP.entry_value); try leb.writeUleb128(writer, expression.len); try writer.writeAll(expression); } // 2.6: Location Descriptions pub fn writeReg(writer: *Writer, register: u8) !void { try writer.writeByte(OP.reg0 + register); } pub fn writeRegx(writer: *Writer, register: anytype) !void { try writer.writeByte(OP.regx); try leb.writeUleb128(writer, register); } pub fn writeImplicitValue(writer: *Writer, value_bytes: []const u8) !void { try writer.writeByte(OP.implicit_value); try leb.writeUleb128(writer, value_bytes.len); try writer.writeAll(value_bytes); } }; } /// Certain opcodes are not allowed in a CFA context, see 6.4.2 fn isOpcodeValidInCFA(opcode: OP) bool { return switch (opcode) { .addrx, .call2, .call4, .call_ref, .const_type, .constx, .convert, .deref_type, .regval_type, .reinterpret, .push_object_address, .call_frame_cfa, => false, else => true, }; } fn isOpcodeRegisterLocation(opcode: u8) bool { return switch (opcode) { OP.reg0...OP.reg31, OP.regx => true, else => false, }; } const testing = std.testing; test "DWARF expressions" { const allocator = std.testing.allocator; const options = Options{}; var stack_machine = StackMachine(options){}; defer stack_machine.deinit(allocator); const b = Builder(options); var program = std.ArrayList(u8).init(allocator); defer program.deinit(); const writer = program.writer(); // Literals { const context = Context{}; for (0..32) |i| { try b.writeLiteral(writer, @intCast(i)); } _ = try stack_machine.run(program.items, allocator, context, 0); for (0..32) |i| { const expected = 31 - i; try testing.expectEqual(expected, stack_machine.stack.pop().?.generic); } } // Constants { stack_machine.reset(); program.clearRetainingCapacity(); const input = [_]comptime_int{ 1, -1, @as(usize, @truncate(0x0fff)), @as(isize, @truncate(-0x0fff)), @as(usize, @truncate(0x0fffffff)), @as(isize, @truncate(-0x0fffffff)), @as(usize, @truncate(0x0fffffffffffffff)), @as(isize, @truncate(-0x0fffffffffffffff)), @as(usize, @truncate(0x8000000)), @as(isize, @truncate(-0x8000000)), @as(usize, @truncate(0x12345678_12345678)), @as(usize, @truncate(0xffffffff_ffffffff)), @as(usize, @truncate(0xeeeeeeee_eeeeeeee)), }; try b.writeConst(writer, u8, input[0]); try b.writeConst(writer, i8, input[1]); try b.writeConst(writer, u16, input[2]); try b.writeConst(writer, i16, input[3]); try b.writeConst(writer, u32, input[4]); try b.writeConst(writer, i32, input[5]); try b.writeConst(writer, u64, input[6]); try b.writeConst(writer, i64, input[7]); try b.writeConst(writer, u28, input[8]); try b.writeConst(writer, i28, input[9]); try b.writeAddr(writer, input[10]); var mock_compile_unit: std.debug.Dwarf.CompileUnit = undefined; mock_compile_unit.addr_base = 1; var mock_debug_addr = std.ArrayList(u8).init(allocator); defer mock_debug_addr.deinit(); try mock_debug_addr.writer().writeInt(u16, 0, native_endian); try mock_debug_addr.writer().writeInt(usize, input[11], native_endian); try mock_debug_addr.writer().writeInt(usize, input[12], native_endian); const context = Context{ .compile_unit = &mock_compile_unit, .debug_addr = mock_debug_addr.items, }; try b.writeConstx(writer, @as(usize, 1)); try b.writeAddrx(writer, @as(usize, 1 + @sizeOf(usize))); const die_offset: usize = @truncate(0xaabbccdd); const type_bytes: []const u8 = &.{ 1, 2, 3, 4 }; try b.writeConstType(writer, die_offset, type_bytes); _ = try stack_machine.run(program.items, allocator, context, 0); const const_type = stack_machine.stack.pop().?.const_type; try testing.expectEqual(die_offset, const_type.type_offset); try testing.expectEqualSlices(u8, type_bytes, const_type.value_bytes); const expected = .{ .{ usize, input[12], usize }, .{ usize, input[11], usize }, .{ usize, input[10], usize }, .{ isize, input[9], isize }, .{ usize, input[8], usize }, .{ isize, input[7], isize }, .{ usize, input[6], usize }, .{ isize, input[5], isize }, .{ usize, input[4], usize }, .{ isize, input[3], isize }, .{ usize, input[2], usize }, .{ isize, input[1], isize }, .{ usize, input[0], usize }, }; inline for (expected) |e| { try testing.expectEqual(@as(e[0], e[1]), @as(e[2], @bitCast(stack_machine.stack.pop().?.generic))); } } // Register values if (@sizeOf(std.debug.ThreadContext) != 0) { stack_machine.reset(); program.clearRetainingCapacity(); const reg_context = abi.RegisterContext{ .eh_frame = true, .is_macho = builtin.os.tag == .macos, }; var thread_context: std.debug.ThreadContext = undefined; std.debug.relocateContext(&thread_context); const context = Context{ .thread_context = &thread_context, .reg_context = reg_context, }; // Only test register operations on arch / os that have them implemented if (abi.regBytes(&thread_context, 0, reg_context)) |reg_bytes| { // TODO: Test fbreg (once implemented): mock a DIE and point compile_unit.frame_base at it mem.writeInt(usize, reg_bytes[0..@sizeOf(usize)], 0xee, native_endian); (try abi.regValueNative(&thread_context, abi.fpRegNum(native_arch, reg_context), reg_context)).* = 1; (try abi.regValueNative(&thread_context, abi.spRegNum(native_arch, reg_context), reg_context)).* = 2; (try abi.regValueNative(&thread_context, abi.ipRegNum(native_arch).?, reg_context)).* = 3; try b.writeBreg(writer, abi.fpRegNum(native_arch, reg_context), @as(usize, 100)); try b.writeBreg(writer, abi.spRegNum(native_arch, reg_context), @as(usize, 200)); try b.writeBregx(writer, abi.ipRegNum(native_arch).?, @as(usize, 300)); try b.writeRegvalType(writer, @as(u8, 0), @as(usize, 400)); _ = try stack_machine.run(program.items, allocator, context, 0); const regval_type = stack_machine.stack.pop().?.regval_type; try testing.expectEqual(@as(usize, 400), regval_type.type_offset); try testing.expectEqual(@as(u8, @sizeOf(usize)), regval_type.type_size); try testing.expectEqual(@as(usize, 0xee), regval_type.value); try testing.expectEqual(@as(usize, 303), stack_machine.stack.pop().?.generic); try testing.expectEqual(@as(usize, 202), stack_machine.stack.pop().?.generic); try testing.expectEqual(@as(usize, 101), stack_machine.stack.pop().?.generic); } else |err| { switch (err) { error.UnimplementedArch, error.UnimplementedOs, error.ThreadContextNotSupported, => {}, else => return err, } } } // Stack operations { var context = Context{}; stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u8, 1); try b.writeOpcode(writer, OP.dup); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 1), stack_machine.stack.pop().?.generic); try testing.expectEqual(@as(usize, 1), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u8, 1); try b.writeOpcode(writer, OP.drop); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expect(stack_machine.stack.pop() == null); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u8, 4); try b.writeConst(writer, u8, 5); try b.writeConst(writer, u8, 6); try b.writePick(writer, 2); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 4), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u8, 4); try b.writeConst(writer, u8, 5); try b.writeConst(writer, u8, 6); try b.writeOpcode(writer, OP.over); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 5), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u8, 5); try b.writeConst(writer, u8, 6); try b.writeOpcode(writer, OP.swap); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 5), stack_machine.stack.pop().?.generic); try testing.expectEqual(@as(usize, 6), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u8, 4); try b.writeConst(writer, u8, 5); try b.writeConst(writer, u8, 6); try b.writeOpcode(writer, OP.rot); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 5), stack_machine.stack.pop().?.generic); try testing.expectEqual(@as(usize, 4), stack_machine.stack.pop().?.generic); try testing.expectEqual(@as(usize, 6), stack_machine.stack.pop().?.generic); const deref_target: usize = @truncate(0xffeeffee_ffeeffee); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeAddr(writer, @intFromPtr(&deref_target)); try b.writeOpcode(writer, OP.deref); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(deref_target, stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeLiteral(writer, 0); try b.writeAddr(writer, @intFromPtr(&deref_target)); try b.writeOpcode(writer, OP.xderef); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(deref_target, stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeAddr(writer, @intFromPtr(&deref_target)); try b.writeDerefSize(writer, 1); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeLiteral(writer, 0); try b.writeAddr(writer, @intFromPtr(&deref_target)); try b.writeXDerefSize(writer, 1); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), stack_machine.stack.pop().?.generic); const type_offset: usize = @truncate(0xaabbaabb_aabbaabb); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeAddr(writer, @intFromPtr(&deref_target)); try b.writeDerefType(writer, 1, type_offset); _ = try stack_machine.run(program.items, allocator, context, null); const deref_type = stack_machine.stack.pop().?.regval_type; try testing.expectEqual(type_offset, deref_type.type_offset); try testing.expectEqual(@as(u8, 1), deref_type.type_size); try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), deref_type.value); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeLiteral(writer, 0); try b.writeAddr(writer, @intFromPtr(&deref_target)); try b.writeXDerefType(writer, 1, type_offset); _ = try stack_machine.run(program.items, allocator, context, null); const xderef_type = stack_machine.stack.pop().?.regval_type; try testing.expectEqual(type_offset, xderef_type.type_offset); try testing.expectEqual(@as(u8, 1), xderef_type.type_size); try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), xderef_type.value); context.object_address = &deref_target; stack_machine.reset(); program.clearRetainingCapacity(); try b.writeOpcode(writer, OP.push_object_address); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, @intFromPtr(context.object_address.?)), stack_machine.stack.pop().?.generic); // TODO: Test OP.form_tls_address context.cfa = @truncate(0xccddccdd_ccddccdd); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeOpcode(writer, OP.call_frame_cfa); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(context.cfa.?, stack_machine.stack.pop().?.generic); } // Arithmetic and Logical Operations { const context = Context{}; stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, i16, -4096); try b.writeOpcode(writer, OP.abs); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 4096), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 0xff0f); try b.writeConst(writer, u16, 0xf0ff); try b.writeOpcode(writer, OP.@"and"); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 0xf00f), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, i16, -404); try b.writeConst(writer, i16, 100); try b.writeOpcode(writer, OP.div); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(isize, -404 / 100), @as(isize, @bitCast(stack_machine.stack.pop().?.generic))); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 200); try b.writeConst(writer, u16, 50); try b.writeOpcode(writer, OP.minus); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 150), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 123); try b.writeConst(writer, u16, 100); try b.writeOpcode(writer, OP.mod); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 23), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 0xff); try b.writeConst(writer, u16, 0xee); try b.writeOpcode(writer, OP.mul); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 0xed12), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 5); try b.writeOpcode(writer, OP.neg); try b.writeConst(writer, i16, -6); try b.writeOpcode(writer, OP.neg); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 6), stack_machine.stack.pop().?.generic); try testing.expectEqual(@as(isize, -5), @as(isize, @bitCast(stack_machine.stack.pop().?.generic))); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 0xff0f); try b.writeOpcode(writer, OP.not); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(~@as(usize, 0xff0f), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 0xff0f); try b.writeConst(writer, u16, 0xf0ff); try b.writeOpcode(writer, OP.@"or"); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 0xffff), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, i16, 402); try b.writeConst(writer, i16, 100); try b.writeOpcode(writer, OP.plus); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 502), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 4096); try b.writePlusUconst(writer, @as(usize, 8192)); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 4096 + 8192), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 0xfff); try b.writeConst(writer, u16, 1); try b.writeOpcode(writer, OP.shl); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 0xfff << 1), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 0xfff); try b.writeConst(writer, u16, 1); try b.writeOpcode(writer, OP.shr); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 0xfff >> 1), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 0xfff); try b.writeConst(writer, u16, 1); try b.writeOpcode(writer, OP.shr); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, @bitCast(@as(isize, 0xfff) >> 1)), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 0xf0ff); try b.writeConst(writer, u16, 0xff0f); try b.writeOpcode(writer, OP.xor); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 0x0ff0), stack_machine.stack.pop().?.generic); } // Control Flow Operations { const context = Context{}; const expected = .{ .{ OP.le, 1, 1, 0 }, .{ OP.ge, 1, 0, 1 }, .{ OP.eq, 1, 0, 0 }, .{ OP.lt, 0, 1, 0 }, .{ OP.gt, 0, 0, 1 }, .{ OP.ne, 0, 1, 1 }, }; inline for (expected) |e| { stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConst(writer, u16, 0); try b.writeConst(writer, u16, 0); try b.writeOpcode(writer, e[0]); try b.writeConst(writer, u16, 0); try b.writeConst(writer, u16, 1); try b.writeOpcode(writer, e[0]); try b.writeConst(writer, u16, 1); try b.writeConst(writer, u16, 0); try b.writeOpcode(writer, e[0]); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, e[3]), stack_machine.stack.pop().?.generic); try testing.expectEqual(@as(usize, e[2]), stack_machine.stack.pop().?.generic); try testing.expectEqual(@as(usize, e[1]), stack_machine.stack.pop().?.generic); } stack_machine.reset(); program.clearRetainingCapacity(); try b.writeLiteral(writer, 2); try b.writeSkip(writer, 1); try b.writeLiteral(writer, 3); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 2), stack_machine.stack.pop().?.generic); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeLiteral(writer, 2); try b.writeBra(writer, 1); try b.writeLiteral(writer, 3); try b.writeLiteral(writer, 0); try b.writeBra(writer, 1); try b.writeLiteral(writer, 4); try b.writeLiteral(writer, 5); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 5), stack_machine.stack.pop().?.generic); try testing.expectEqual(@as(usize, 4), stack_machine.stack.pop().?.generic); try testing.expect(stack_machine.stack.pop() == null); // TODO: Test call2, call4, call_ref once implemented } // Type conversions { const context = Context{}; stack_machine.reset(); program.clearRetainingCapacity(); // TODO: Test typed OP.convert once implemented const value: usize = @truncate(0xffeeffee_ffeeffee); var value_bytes: [options.addr_size]u8 = undefined; mem.writeInt(usize, &value_bytes, value, native_endian); // Convert to generic type stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConstType(writer, @as(usize, 0), &value_bytes); try b.writeConvert(writer, @as(usize, 0)); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(value, stack_machine.stack.pop().?.generic); // Reinterpret to generic type stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConstType(writer, @as(usize, 0), &value_bytes); try b.writeReinterpret(writer, @as(usize, 0)); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(value, stack_machine.stack.pop().?.generic); // Reinterpret to new type const die_offset: usize = 0xffee; stack_machine.reset(); program.clearRetainingCapacity(); try b.writeConstType(writer, @as(usize, 0), &value_bytes); try b.writeReinterpret(writer, die_offset); _ = try stack_machine.run(program.items, allocator, context, null); const const_type = stack_machine.stack.pop().?.const_type; try testing.expectEqual(die_offset, const_type.type_offset); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeLiteral(writer, 0); try b.writeReinterpret(writer, die_offset); _ = try stack_machine.run(program.items, allocator, context, null); const regval_type = stack_machine.stack.pop().?.regval_type; try testing.expectEqual(die_offset, regval_type.type_offset); } // Special operations { var context = Context{}; stack_machine.reset(); program.clearRetainingCapacity(); try b.writeOpcode(writer, OP.nop); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expect(stack_machine.stack.pop() == null); // Sub-expression { var sub_program = std.ArrayList(u8).init(allocator); defer sub_program.deinit(); const sub_writer = sub_program.writer(); try b.writeLiteral(sub_writer, 3); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeEntryValue(writer, sub_program.items); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 3), stack_machine.stack.pop().?.generic); } // Register location description const reg_context = abi.RegisterContext{ .eh_frame = true, .is_macho = builtin.os.tag == .macos, }; var thread_context: std.debug.ThreadContext = undefined; std.debug.relocateContext(&thread_context); context = Context{ .thread_context = &thread_context, .reg_context = reg_context, }; if (abi.regBytes(&thread_context, 0, reg_context)) |reg_bytes| { mem.writeInt(usize, reg_bytes[0..@sizeOf(usize)], 0xee, native_endian); var sub_program = std.ArrayList(u8).init(allocator); defer sub_program.deinit(); const sub_writer = sub_program.writer(); try b.writeReg(sub_writer, 0); stack_machine.reset(); program.clearRetainingCapacity(); try b.writeEntryValue(writer, sub_program.items); _ = try stack_machine.run(program.items, allocator, context, null); try testing.expectEqual(@as(usize, 0xee), stack_machine.stack.pop().?.generic); } else |err| { switch (err) { error.UnimplementedArch, error.UnimplementedOs, error.ThreadContextNotSupported, => {}, else => return err, } } } }