diff --git a/perforator/agent/collector/pkg/profile/builder.go b/perforator/agent/collector/pkg/profile/builder.go index 87540d3f8..7a0e94136 100644 --- a/perforator/agent/collector/pkg/profile/builder.go +++ b/perforator/agent/collector/pkg/profile/builder.go @@ -481,6 +481,8 @@ type InterpreterLocationKey struct { Linestart int32 Line int32 Language models.Language + // Lua function names depend on the caller, even for the same proto and line. + Name string } type functionKey struct { diff --git a/perforator/agent/collector/pkg/profiler/native_runtime.go b/perforator/agent/collector/pkg/profiler/native_runtime.go index deb0fe7b3..31da975e2 100644 --- a/perforator/agent/collector/pkg/profiler/native_runtime.go +++ b/perforator/agent/collector/pkg/profiler/native_runtime.go @@ -77,7 +77,7 @@ func (r *nativeRuntimeResolver) resolveInterpreterSection(key linux.ProcessKey, if r.lua == nil { return fmt.Errorf("interpreter resolver is unavailable") } - err = resolveInterpreterSection(r.lua.ResolveFrame, key, section, writer) + err = resolveLuaSection(r.lua.ResolveFrame, key, section, writer) default: return fmt.Errorf("unsupported interpreter language %d", section.Language) } @@ -203,6 +203,31 @@ func resolveInterpreterSection[T runtimeFrame](resolve func(linux.ProcessKey, *T return nil } +func resolveLuaSection(resolve func(linux.ProcessKey, *unwinder.LuaFrame, *unwinder.LuaFrame, runtimewriter.Frame) error, key linux.ProcessKey, section nativepipeline.LanguageSection, writer *runtimewriter.Writer) error { + frames, err := viewRuntimeFrames[unwinder.LuaFrame](section) + if err != nil { + return err + } + for i := range frames { + output, err := writer.AddFrame(0, 1) + if err != nil { + return err + } + + var callerFrame *unwinder.LuaFrame + + // Frames run from callee to caller. The caller's PC names callee's frame. + if i+1 < len(frames) { + callerFrame = &frames[i+1] + } + + if err := resolve(key, &frames[i], callerFrame, output); err != nil { + return err + } + } + return nil +} + // Frames borrow the BPF payload for the synchronous resolver call. func viewRuntimeFrames[T runtimeFrame | unwinder.JvmFrame](section nativepipeline.LanguageSection) ([]T, error) { var frame T diff --git a/perforator/agent/collector/progs/unwinder/arch/x86/regs.h b/perforator/agent/collector/progs/unwinder/arch/x86/regs.h index 884bfa22e..fc2549efc 100644 --- a/perforator/agent/collector/progs/unwinder/arch/x86/regs.h +++ b/perforator/agent/collector/progs/unwinder/arch/x86/regs.h @@ -17,6 +17,7 @@ struct user_regs { u64 rdi; u64 rdx; u64 r14; + u64 rbx; }; static ALWAYS_INLINE u64 regs_get_current_instruction(struct user_regs* regs) { @@ -30,6 +31,7 @@ struct pt_regs___kernel { u64 di; u64 dx; u64 r14; + u64 bx; }; //////////////////////////////////////////////////////////////////////////////// @@ -75,6 +77,7 @@ static NOINLINE bool extract_saved_userspace_registers(struct user_regs* regs) { regs->rdi = BPF_CORE_READ(kregs, di); regs->rdx = BPF_CORE_READ(kregs, dx); regs->r14 = BPF_CORE_READ(kregs, r14); + regs->rbx = BPF_CORE_READ(kregs, bx); return true; } @@ -90,6 +93,7 @@ static NOINLINE bool find_task_userspace_registers(struct pt_regs* kregs, struct uregs->rdi = kregs->rdi; uregs->rdx = kregs->rdx; uregs->r14 = kregs->r14; + uregs->rbx = kregs->rbx; return true; } diff --git a/perforator/agent/collector/progs/unwinder/lua/frame.h b/perforator/agent/collector/progs/unwinder/lua/frame.h index 1551f7b7c..c8c8aef56 100644 --- a/perforator/agent/collector/progs/unwinder/lua/frame.h +++ b/perforator/agent/collector/progs/unwinder/lua/frame.h @@ -1,41 +1,97 @@ #pragma once #include "luajit.h" +#include "stack/context.h" #include "symbol.h" #include "trace.h" // namespace lua::frame +/** + * @brief Small context for current frame. + */ +struct lua_frame_context { + luajit_tvalue* frame; // Current frame. + luajit_tvalue* next_frame; // Frame above the current. + luajit_gc_func* frame_function; // Current frame function. +}; + /** * @brief Checks if this frame already has a saved symbol in the symbol cache. * - * @param lua_frame_key Lua frame key. + * @param proto Proto from lua function in the frame. + * @param first_line First line where proto is defined. + * @param cache_key Key for symbol cache. * * @return `true` if already saved this symbol */ static ALWAYS_INLINE bool lua_frame_has_symbol( - struct symbol_key* lua_frame_key, + luajit_gc_proto* proto, + i32 first_line, struct interpreter_symbol_key* cache_key ) { - cache_key->symbol_key = *lua_frame_key; + cache_key->symbol_key = (struct symbol_key) { + .object_addr = (u64)proto, + .linestart = first_line, + }; return bpf_map_lookup_elem(&interpreter_symbols, cache_key) != NULL; } /** * @brief Save symbol info for the frame to the map. * - * @param lua_frame_key Lua frame key. + * @param proto Proto from lua function in the frame. + * @param first_line First line where proto is defined. + * @param cache_key Key for symbol cache. * @param symbol Symbol info. */ static ALWAYS_INLINE void lua_frame_save_symbol( - struct symbol_key* lua_frame_key, + luajit_gc_proto* proto, + i32 first_line, struct interpreter_symbol_key* cache_key, struct symbol* symbol ) { - cache_key->symbol_key = *lua_frame_key; + cache_key->symbol_key = (struct symbol_key) { + .object_addr = (u64)proto, + .linestart = first_line, + }; bpf_map_update_elem(&interpreter_symbols, cache_key, symbol, BPF_ANY); } +/** + * @brief Check the top vararg frame for correct initialization. + * + * Sample might interrupt in-between vararg function which has 2 frames: `FRAME_VARG` and `FRAME_*`. + * If the top frame is not `FRAME_VARG` but holds a vararg function, it should be skipped. + * + * @param frame Top frame. + * @return `true` is first frame is valid vararg or not a vararg + */ +static ALWAYS_INLINE bool lua_frame_should_skip_vararg(const luajit_tvalue* frame) { + if (luajit_frame_isvarg(frame)) { + return true; + } + + luajit_gc_func* frame_function = (luajit_gc_func*)luajit_frame_func(frame); + if (frame_function == NULL) { + LUA_LOG_ERROR("First frame=%px function is NULL", frame); + return true; + } + + if (!luajit_isluafunc(frame_function)) { + return true; + } + + luajit_gc_proto* proto = luajit_funcproto(frame_function); + if (proto == NULL) { + LUA_LOG_ERROR("First frame=%px function=%px proto is NULL", frame, frame_function); + return false; + } + + // Function must not have a vararg flag + return (luajit_gc_proto_get_flags(proto) & LUAJIT_PROTO_VARARG) == 0; +} + /** * @brief Set frame as invalid frame. * @@ -47,59 +103,181 @@ static ALWAYS_INLINE void lua_frame_set_invalid(struct lua_frame* lua_frame, enu .type = LUA_FRAME_TYPE_INVALID, .value.invalid_frame.error = error, }; + + LUA_LOG_DEBUG("Invalid frame error=%d", error); +} + +/** + * @brief Get bytecode position in proto. + * + * @param proto Current frame `GCproto`. + * @param bytecode_pc Pointer to bytecode. + * @return u32 Bytecode position or `LUAJIT_NO_BCPOS`. + */ +static ALWAYS_INLINE u32 luajit_gc_proto_get_pc_position(luajit_gc_proto* proto, const u32* bytecode_pc) { + const u32* proto_bc = luajit_gc_proto_bc(proto); + if (!bytecode_pc || bytecode_pc <= proto_bc) { + return LUAJIT_NO_BCPOS; + } + + u64 offset = (u64)bytecode_pc - (u64)proto_bc; + if (offset % sizeof(u32) || offset > luajit_gc_proto_get_sizebc(proto) * sizeof(u32)) { + return LUAJIT_NO_BCPOS; + } + + return luajit_gc_proto_bcpos(proto, bytecode_pc) - 1; +} + +/** + * @brief Return bytecode position for Lua function in the current frame or `LUAJIT_NO_BCPOS`. + * + * @see `debug_framepc` from LuaJIT source code. + * + * @param frame_context Current frame context. + * @param proto Current frame `GCproto`. + * @param state Current `lua_State`. + * @param bytecode_pc_register Register possibly containing the bytecode PC. + * @return u32 Last bytecode position in the frame or `LUAJIT_NO_BCPOS`. + */ +static ALWAYS_INLINE u32 lua_frame_get_pc(struct lua_frame_context* frame_context, luajit_gc_proto* proto, luajit_state* lua_state, u32* bytecode_pc_register) { + const u32* pc; + + const luajit_tvalue* next_frame = frame_context->next_frame; + if (next_frame == NULL) { + // When the top frame is a Lua function, it usually doesn't leave ASM VM. + // If we interruped inside ASM VM, we might have a pointer to current bytecode program counter in designated register. + u32 pos = luajit_gc_proto_get_pc_position(proto, bytecode_pc_register); + if (pos != LUAJIT_NO_BCPOS) { + LUA_LOG_DEBUG("Bytecode pc register=%px was used to determine current pc for frame_function=%px frame=%px", bytecode_pc_register, frame_context->frame_function, frame_context->frame); + return pos; + } else { + LUA_LOG_DEBUG("Bytecode pc register=%px was out of bounds for frame_function=%px frame=%px", bytecode_pc_register, frame_context->frame_function, frame_context->frame); + } + + luajit_cframe* cf = luajit_cframe_raw(luajit_state_get_cframe(lua_state)); + if (cf == NULL || luajit_cframe_get_pc(cf) == (u32*)luajit_cframe_get_l(cf)) { + return LUAJIT_NO_BCPOS; + } + + pc = luajit_cframe_get_pc(cf); + } else if (luajit_frame_islua(next_frame)) { + pc = luajit_frame_pc(next_frame); + } else if (luajit_frame_iscont(next_frame)) { + pc = luajit_frame_contpc(next_frame); + } else { + // Not implemented yet. Lua function below errfunc/gc/hook. Not a hot path. + LUA_DEV_ASSERT(false, "Lua function below errfunc/gc/hook"); + return LUAJIT_NO_BCPOS; + } + + // TODO: JIT synthetic PCs. + + return luajit_gc_proto_get_pc_position(proto, pc); +} + +/** + * @brief Get line number for a bytecode position. + * + * @see `lj_debug_line` from LuaJIT source code. + * + * @param proto Current frame `GCproto`. + * @param bytecode_pc Bytecode PC. + * @return Line for given bytecode PC + */ +[[nodiscard]] static ALWAYS_INLINE i32 luajit_gc_proto_get_pc_line(luajit_gc_proto* proto, u32 bytecode_pc) { + const void* lineinfo = luajit_gc_proto_get_lineinfo(proto); + u32 sizebc = luajit_gc_proto_get_sizebc(proto); + + if (!lineinfo || bytecode_pc > sizebc) { + return 0; + } + + i32 first = luajit_gc_proto_get_firstline(proto); + i32 numline = luajit_gc_proto_get_numline(proto); + + if (bytecode_pc == sizebc) { + return first + numline; + } + + if (bytecode_pc-- == 0) { + return first; + } + + if (numline < 256) { + return first + (i32)BPF_PROBE_READ_USER_FROM(&((u8*)lineinfo)[bytecode_pc]); + } + + if (numline < 65536) { + return first + (i32)BPF_PROBE_READ_USER_FROM(&((u16*)lineinfo)[bytecode_pc]); + } + + return first + (i32)BPF_PROBE_READ_USER_FROM(&((u32*)lineinfo)[bytecode_pc]); } /** * @brief Set frame as Lua frame. * - * @param lua_frame Current frame. - * @param symbol Symbol from state. - * @param function Lua function. - * @return `true` if set successfully, `false` if failed to get function proto. + * @param stack_context Stack context. + * @param frame_context Current frame context. + * @return Currently always `true`. */ [[nodiscard]] static ALWAYS_INLINE bool lua_frame_set_lua( - struct lua_frame* lua_frame, - struct interpreter_symbol_key* cache_key, - struct symbol* symbol, - luajit_gc_func* function + struct lua_stack_context* stack_context, + struct lua_frame_context* frame_context ) { - luajit_gc_proto* proto = luajit_funcproto(function); - i32 line_defined = luajit_gc_proto_get_firstline(proto); + luajit_gc_proto* proto = luajit_funcproto(frame_context->frame_function); + i32 first_line = luajit_gc_proto_get_firstline(proto); + u32 pc = lua_frame_get_pc( + frame_context, proto, + (luajit_state*)stack_context->lua_state, + (u32*)stack_context->bytecode_pc_register + ); + LUA_LOG_DEBUG("Lua frame function=%px pc=%d", frame_context->frame_function, pc); + + struct lua_frame* lua_frame = &stack_context->lua_frame; *lua_frame = (struct lua_frame) { .type = LUA_FRAME_TYPE_LUA, .value.lua_frame = { - .object_addr = (u64)proto, - .linestart = line_defined, + .proto_address = (u64)proto, + .first_line = first_line, + .current_line = pc == LUAJIT_NO_BCPOS ? first_line : luajit_gc_proto_get_pc_line(proto, pc), + .bytecode_index = pc, }, }; + LUA_LOG_DEBUG("Lua frame function=%px proto=%px current_line=%d", frame_context->frame_function, proto, lua_frame->value.lua_frame.current_line); - if (lua_frame_has_symbol(&lua_frame->value.lua_frame, cache_key)) { + if (lua_frame_has_symbol(proto, first_line, &stack_context->symbol_cache_key)) { + LUA_LOG_DEBUG("Lua frame symbol cache hit proto=%px", proto); return true; } - char* caret = symbol->data; - u8 name_length = 0; + struct symbol* symbol = &stack_context->symbol; - if (!line_defined && luajit_gc_proto_get_numline(proto)) { - name_length = (u8)LUA_SYMBOL_APPEND_LITERAL(symbol->data, "in main chunk"); + // Writing GCproto struct into symbol buffer as `name`. It fits in size and we don't use name part. + // symbol structure: [gcproto][filename] + long status = bpf_probe_read_user(symbol->data, LUAJIT_GC_PROTO_SIZEOF, proto); + if (status != 0) { + LUA_LOG_ERROR("Failed to read proto=%px struct (%d)", proto, status); + // Leave the symbol uncached so a later sample can retry. + return true; } - - symbol->name_length = name_length; - caret += name_length; + symbol->name_length = LUAJIT_GC_PROTO_SIZEOF; // This must be written inline for least amount of verifier checks const char* filename = luajit_proto_chunknamestr(proto); - long status = bpf_probe_read_user_str(caret, SYMBOL_BUFFER_SIZE - name_length, filename); + char* filename_data = symbol->data + LUAJIT_GC_PROTO_SIZEOF; + status = bpf_probe_read_user_str(filename_data, LUA_SYMBOL_MAX_FILENAME_LENGTH + 1, filename); if (status <= 0) { - LUA_TRACE("[error] lua_frame_set_lua: failed to read proto=%px filename (%d)", proto, status); - symbol->filename_length = (u8)lua_symbol_append_fail(caret); + LUA_LOG_ERROR("Failed to read proto=%px filename (%d)", proto, status); + symbol->filename_length = (u8)lua_symbol_append_fail(filename_data); } else { --status; - symbol->filename_length = status > 255 ? 255 : (u8)status; + symbol->filename_length = (u8)status; } - lua_frame_save_symbol(&lua_frame->value.lua_frame, cache_key, symbol); + LUA_LOG_DEBUG("Saved symbol for Lua frame proto=%px filename_length=%d", proto, symbol->filename_length); + lua_frame_save_symbol(proto, first_line, &stack_context->symbol_cache_key, symbol); return true; } @@ -114,8 +292,10 @@ static ALWAYS_INLINE void lua_frame_set_c(struct lua_frame* lua_frame, luajit_gc *lua_frame = (struct lua_frame) { .type = LUA_FRAME_TYPE_C, .value.c_frame = { - .object_addr = (u64)luajit_gc_func_get_f(function), + .function_address = (u64)luajit_gc_func_get_f(function), .ffid = luajit_gc_func_get_ffid(function), }, }; + + LUA_LOG_DEBUG("C frame function=%px function_address=%px ffid=%d", function, lua_frame->value.c_frame.function_address, lua_frame->value.c_frame.ffid); } diff --git a/perforator/agent/collector/progs/unwinder/lua/luajit.h b/perforator/agent/collector/progs/unwinder/lua/luajit.h index 2d9574148..70f300b56 100644 --- a/perforator/agent/collector/progs/unwinder/lua/luajit.h +++ b/perforator/agent/collector/progs/unwinder/lua/luajit.h @@ -33,33 +33,58 @@ enum { // Tagged value. // TValue typedef u64 luajit_tvalue; +typedef struct luajit_gc_obj luajit_gc_obj; // Predefining static const u32 LUAJIT_LJ_TFUNC = (~8u); static const u64 LUAJIT_LJ_GCVMASK = (1ULL << 47) - 1; // GCstr +typedef struct luajit_gc_str luajit_gc_str; enum { LUAJIT_GC_STR_SIZEOF = 24, // sizeof(GCstr) }; +[[nodiscard]] static ALWAYS_INLINE const char* luajit_strdata(luajit_gc_str* string) { + // #define strdata(s) ((const char *)((s)+1)) + return (const char*)LUAJIT_GET_OFFSET(string, LUAJIT_GC_STR_SIZEOF); +} + // GCproto typedef struct luajit_gc_proto luajit_gc_proto; enum { LUAJIT_GC_PROTO_SIZEOF = 104, // sizeof(GCproto) + LUAJIT_GC_PROTO_SIZEBC = 12, // MSize sizebc; + LUAJIT_GC_PROTO_FLAGS = 61, // uint8_t flags; LUAJIT_GC_PROTO_CHUNKNAME = 64, // GCRef chunkname; LUAJIT_GC_PROTO_FIRSTLINE = 72, // BCLine firstline; LUAJIT_GC_PROTO_NUMLINE = 76, // BCLine numline; + LUAJIT_GC_PROTO_LINEINFO = 80, // MRef lineinfo; }; -LUAJIT_DEFINE_FIELD_GETTER(void*, luajit_gc_proto, chunkname, LUAJIT_GC_PROTO_CHUNKNAME); + +static const int LUAJIT_PROTO_VARARG = 0x02; // Vararg function. + +LUAJIT_DEFINE_FIELD_GETTER(u32, luajit_gc_proto, sizebc, LUAJIT_GC_PROTO_SIZEBC); +LUAJIT_DEFINE_FIELD_GETTER(u8, luajit_gc_proto, flags, LUAJIT_GC_PROTO_FLAGS); +LUAJIT_DEFINE_FIELD_GETTER(luajit_gc_str*, luajit_gc_proto, chunkname, LUAJIT_GC_PROTO_CHUNKNAME); LUAJIT_DEFINE_FIELD_GETTER(i32, luajit_gc_proto, firstline, LUAJIT_GC_PROTO_FIRSTLINE); LUAJIT_DEFINE_FIELD_GETTER(i32, luajit_gc_proto, numline, LUAJIT_GC_PROTO_NUMLINE); +LUAJIT_DEFINE_FIELD_GETTER(const void*, luajit_gc_proto, lineinfo, LUAJIT_GC_PROTO_LINEINFO); + +[[nodiscard]] static ALWAYS_INLINE u32* luajit_gc_proto_bc(luajit_gc_proto* proto) { + // #define proto_bc(pt) ((BCIns *)((char *)(pt) + sizeof(GCproto))) + return (u32*)((char*)(proto) + LUAJIT_GC_PROTO_SIZEOF); +} + +[[nodiscard]] static ALWAYS_INLINE u32 luajit_gc_proto_bcpos(luajit_gc_proto* proto, const u32* pc) { + // #define proto_bcpos(pt, pc) ((BCPos)((pc) - proto_bc(pt))) + return (u32)((pc)-luajit_gc_proto_bc(proto)); +} [[nodiscard]] static ALWAYS_INLINE const char* luajit_proto_chunknamestr(luajit_gc_proto* proto) { // #define strref(r) (&gcref((r))->str) // #define proto_chunkname(pt) (strref((pt)->chunkname)) - // #define strdata(s) ((const char *)((s)+1)) // #define proto_chunknamestr(pt) (strdata(proto_chunkname((pt)))) - return (char*)luajit_gc_proto_get_chunkname(proto) + LUAJIT_GC_STR_SIZEOF; + return luajit_strdata(luajit_gc_proto_get_chunkname(proto)); } // GCfunc @@ -82,7 +107,7 @@ static const int LUAJIT_FF_LUA = 0; [[nodiscard]] static ALWAYS_INLINE luajit_gc_proto* luajit_funcproto(luajit_gc_func* function) { // #define funcproto(fn) (GCproto *)(mref((fn)->l.pc, char)-sizeof(GCproto)) - return (luajit_gc_proto*)((char*)(luajit_gc_func_get_pc(function)) - LUAJIT_GC_PROTO_SIZEOF); + return (luajit_gc_proto*)LUAJIT_GET_OFFSET(luajit_gc_func_get_pc(function), -LUAJIT_GC_PROTO_SIZEOF); } enum { @@ -159,7 +184,7 @@ LUAJIT_DEFINE_FIELD_GETTER(luajit_tvalue*, luajit_state, stack, LUAJIT_LUA_STATE LUAJIT_DEFINE_FIELD_GETTER(void*, luajit_state, cframe, LUAJIT_LUA_STATE_CFRAME); // GCobj -typedef union luajit_gc_obj luajit_gc_obj; +typedef struct luajit_gc_obj luajit_gc_obj; [[nodiscard]] static ALWAYS_INLINE u32 luajit_itype(const luajit_tvalue* value) { // #define itype(o) ((uint32_t)((o)->it64 >> 47)) @@ -179,22 +204,32 @@ typedef union luajit_gc_obj luajit_gc_obj; // lj_bc.h +static const u32 LUAJIT_NO_BCPOS = ~0u; // Invalid bytecode position. + [[nodiscard]] static ALWAYS_INLINE u32 luajit_bc_a(u32 instruction) { // #define bc_a(i) ((BCReg)(((i)>>8)&0xff)) - return (u32)((instruction >> 8) & 0xff); + return (instruction >> 8) & 0xff; } // lj_frame.h -static const int LUAJIT_FRAME_LUA = 0; -static const int LUAJIT_FRAME_VARG = 3; +enum luajit_frame_type { + LUAJIT_FRAME_LUA, // Lua frame + LUAJIT_FRAME_C, // C frame + LUAJIT_FRAME_CONT, // Continuation frame + LUAJIT_FRAME_VARG, // Lua vararg frame + LUAJIT_FRAME_LUAP, // Lua protected??? + LUAJIT_FRAME_CP, // cpcall() frame + LUAJIT_FRAME_PCALL, // ff pcall() frame + LUAJIT_FRAME_PCALLH // ff pcall() frame with active hook +}; static const int LUAJIT_FRAME_TYPE = 3; static const int LUAJIT_FRAME_P = 4; static const int LUAJIT_FRAME_TYPEP = (LUAJIT_FRAME_TYPE | LUAJIT_FRAME_P); -[[nodiscard]] static ALWAYS_INLINE luajit_gc_obj* luajit_frame_gc(const luajit_tvalue* frame) { +[[nodiscard]] static ALWAYS_INLINE luajit_gc_func* luajit_frame_func(const luajit_tvalue* frame) { // #define frame_gc(f) (gcval((f)-1)) - return luajit_gcval(frame - LUAJIT_LJ_FR2); + return (luajit_gc_func*)luajit_gcval(frame - LUAJIT_LJ_FR2); } [[nodiscard]] static ALWAYS_INLINE i64 luajit_frame_ftsz(const luajit_tvalue* frame) { @@ -207,12 +242,12 @@ static const int LUAJIT_FRAME_TYPEP = (LUAJIT_FRAME_TYPE | LUAJIT_FRAME_P); return (u32*)luajit_frame_ftsz(frame); } -[[nodiscard]] static ALWAYS_INLINE i64 luajit_frame_type(const luajit_tvalue* frame) { +[[nodiscard]] static ALWAYS_INLINE enum luajit_frame_type luajit_frame_type(const luajit_tvalue* frame) { // #define frame_type(f) (frame_ftsz(f) & FRAME_TYPE) return luajit_frame_ftsz(frame) & LUAJIT_FRAME_TYPE; } -[[nodiscard]] static ALWAYS_INLINE i64 luajit_frame_typep(const luajit_tvalue* frame) { +[[nodiscard]] static ALWAYS_INLINE enum luajit_frame_type luajit_frame_typep(const luajit_tvalue* frame) { // #define frame_typep(f) (frame_ftsz(f) & FRAME_TYPEP) return luajit_frame_ftsz(frame) & LUAJIT_FRAME_TYPEP; } @@ -222,6 +257,11 @@ static const int LUAJIT_FRAME_TYPEP = (LUAJIT_FRAME_TYPE | LUAJIT_FRAME_P); return luajit_frame_type(frame) == LUAJIT_FRAME_LUA; } +[[nodiscard]] static ALWAYS_INLINE bool luajit_frame_iscont(const luajit_tvalue* frame) { + // #define frame_iscont(f) (frame_typep(f) == FRAME_CONT) + return luajit_frame_typep(frame) == LUAJIT_FRAME_CONT; +} + [[nodiscard]] static ALWAYS_INLINE bool luajit_frame_isvarg(const luajit_tvalue* frame) { // #define frame_isvarg(f) (frame_typep(f) == FRAME_VARG) return luajit_frame_typep(frame) == LUAJIT_FRAME_VARG; @@ -232,6 +272,11 @@ static const int LUAJIT_FRAME_TYPEP = (LUAJIT_FRAME_TYPE | LUAJIT_FRAME_P); return luajit_frame_ftsz(frame) & ~LUAJIT_FRAME_TYPEP; } +[[nodiscard]] static ALWAYS_INLINE u32* luajit_frame_contpc(const luajit_tvalue* frame) { + // #define frame_contpc(f) (frame_pc((f)-2)) + return luajit_frame_pc(frame - 2); +} + [[nodiscard]] static ALWAYS_INLINE const luajit_tvalue* luajit_frame_prevl(const luajit_tvalue* frame) { // #define frame_prevl(f) ((f) - (1+LJ_FR2+bc_a(frame_pc(f)[-1]))) return (frame) - (1 + LUAJIT_LJ_FR2 + luajit_bc_a(BPF_PROBE_READ_USER_FROM(&luajit_frame_pc(frame)[-1]))); @@ -241,3 +286,35 @@ static const int LUAJIT_FRAME_TYPEP = (LUAJIT_FRAME_TYPE | LUAJIT_FRAME_P); // #define frame_prevd(f) ((TValue *)((char *)(f) - frame_sized(f))) return (const luajit_tvalue*)((char*)(frame)-luajit_frame_sized(frame)); } + +// @note Does not skip over `FRAME_VARG`. +[[nodiscard]] static ALWAYS_INLINE const luajit_tvalue* luajit_frame_prev(const luajit_tvalue* frame) { + // #define frame_prev(f) (frame_islua(f)?frame_prevl(f):frame_prevd(f)) + return luajit_frame_islua(frame) ? luajit_frame_prevl(frame) : luajit_frame_prevd(frame); +} + +typedef struct luajit_cframe luajit_cframe; +enum : u64 { + // CFRAME_OFS_* + LUAJIT_CFRAME_PREV = 4 * 8, + LUAJIT_CFRAME_PC = 3 * 8, + LUAJIT_CFRAME_L = 2 * 8, + LUAJIT_CFRAME_ERRF = 3 * 4, + LUAJIT_CFRAME_NRES = 2 * 4, + LUAJIT_CFRAME_MULTRES = 0 * 4, + + // Other CFRAME_* macros + LUAJIT_CFRAME_RESUME = 1, + LUAJIT_CFRAME_UNWIND_FF = 2, // Only used in unwinder. + LUAJIT_CFRAME_RAWMASK = ~(LUAJIT_CFRAME_RESUME | LUAJIT_CFRAME_UNWIND_FF) +}; +// LUAJIT_DEFINE_FIELD_GETTER(i32, luajit_cframe, errfunc, LUAJIT_CFRAME_ERRF); +// LUAJIT_DEFINE_FIELD_GETTER(i32, luajit_cframe, nres, LUAJIT_CFRAME_NRES); +// LUAJIT_DEFINE_FIELD_GETTER(void*, luajit_cframe, prev, LUAJIT_CFRAME_PREV); +LUAJIT_DEFINE_FIELD_GETTER(luajit_state*, luajit_cframe, l, LUAJIT_CFRAME_L); +LUAJIT_DEFINE_FIELD_GETTER(u32*, luajit_cframe, pc, LUAJIT_CFRAME_PC); + +[[nodiscard]] static ALWAYS_INLINE luajit_cframe* luajit_cframe_raw(void* cframe) { + // #define cframe_raw(cf) ((void *)((intptr_t)(cf) & CFRAME_RAWMASK)) + return (luajit_cframe*)((u64)cframe & LUAJIT_CFRAME_RAWMASK); +} diff --git a/perforator/agent/collector/progs/unwinder/lua/stack/context.h b/perforator/agent/collector/progs/unwinder/lua/stack/context.h index 834a30434..b39da77ca 100644 --- a/perforator/agent/collector/progs/unwinder/lua/stack/context.h +++ b/perforator/agent/collector/progs/unwinder/lua/stack/context.h @@ -3,9 +3,11 @@ #include #include "../../interpreter/types.h" +#include "../../output.h" -#include "../types.h" #include "../luajit.h" +#include "../trace.h" +#include "../types.h" // namespace lua::stack::context @@ -13,13 +15,15 @@ * @brief Stack context used to communicate between BPF functions. */ struct lua_stack_context { - u64 frame; // Current frame. - u64 max_stack; // Last free slot in the stack. - u64 bottom; // Last frame in the stack. - u64 current_lua_state; // Current `lua_State*`. - struct lua_frame lua_frame; // Current interpreter frame. - struct interpreter_symbol_key symbol_cache_key; - struct symbol symbol; // Temporary buffer for frame information. + u64 frame; // Current frame. + u64 next_frame; // Callee frame. + u64 max_stack; // Last free slot in the stack. + u64 bottom; // Last frame in the stack. + u64 lua_state; // Current `lua_State*`. + struct lua_frame lua_frame; // Current interpreter frame. + u64 bytecode_pc_register; // Value of register potentially holding the PC pointer for the top Lua frame. + struct interpreter_symbol_key symbol_cache_key; // Key for Lua frame. + struct symbol symbol; // Temporary buffer for frame information. }; BPF_MAP(lua_stack_context, BPF_MAP_TYPE_PERCPU_ARRAY, u32, struct lua_stack_context, 1); @@ -42,26 +46,64 @@ BPF_MAP(lua_stack_context, BPF_MAP_TYPE_PERCPU_ARRAY, u32, struct lua_stack_cont * @param frame Current frame. * @param max_stack Last free slot in the stack. * @param bottom Last frame in the stack. + * @param bytecode_pc_register Potential PC pointer for the top Lua frame. + * @param pid Current PID. */ static ALWAYS_INLINE void lua_stack_context_init( - struct lua_stack_context* context, - const struct lua_state* state, const luajit_tvalue* frame, - const luajit_tvalue* max_stack, const luajit_tvalue* bottom + struct lua_stack_context* context, const struct lua_state* state, + const luajit_tvalue* frame, const luajit_tvalue* max_stack, + const luajit_tvalue* bottom, u64 bytecode_pc_register ) { context->frame = (u64)frame; + context->next_frame = (u64)NULL; context->max_stack = (u64)max_stack; context->bottom = (u64)bottom; - context->current_lua_state = state->current_lua_state; + context->lua_state = state->current_lua_state; + context->lua_frame = (struct lua_frame) { .type = LUA_FRAME_TYPE_C, .value.c_frame = { - .object_addr = (u64)NULL, + .function_address = (u64)NULL, .ffid = 0, }, }; - context->symbol_cache_key.pid = state->pid; + + context->bytecode_pc_register = bytecode_pc_register; context->symbol_cache_key.process_starttime = state->process_starttime; + context->symbol_cache_key.pid = state->pid; context->symbol_cache_key.language = LANGUAGE_LUA; - __builtin_memset(context->symbol_cache_key._pad, 0, sizeof(context->symbol_cache_key._pad)); + ZERO(context->symbol_cache_key._pad); context->symbol.codepoint_size = 1; // Lua strings are always utf-8 } + +/** + * @brief Get previous (going top to bottom) frame. + * + * Vararg Lua functions has a special treatment because they take 2 frames. + * Both frames will contain the same Lua function but upper frame has varg type and lower the type of frame before. + * + * 0xFF L->base + * 0xF7 frame type varg (`L->base - 1`) + * 0xEF varg Lua function + * ... varg arguments + * 0xCF frame type X (depends on the frame below) + * 0xC7 varg Lua function + * + * To save iterations, skip vararg here, take next frame right away. + * + * @see `lj_debug_frame` from LuaJIT source code. + * + * @param context Stack context. + */ +static ALWAYS_INLINE void lua_stack_context_get_previous_frame(struct lua_stack_context* context) { + const luajit_tvalue* frame = (luajit_tvalue*)context->frame; + + // **Previous** frame is pseudo-frame, but type of the **current** frame is `FRAME_VARG`. + if (luajit_frame_isvarg(frame)) { + LUA_LOG_DEBUG("Skipping vararg pseudo-frame for next iteration"); + frame = luajit_frame_prevd(frame); + } + + context->next_frame = (u64)frame; + context->frame = (u64)luajit_frame_prev(frame); +} diff --git a/perforator/agent/collector/progs/unwinder/lua/stack/stack.h b/perforator/agent/collector/progs/unwinder/lua/stack/stack.h index 15741cad2..484722e2e 100644 --- a/perforator/agent/collector/progs/unwinder/lua/stack/stack.h +++ b/perforator/agent/collector/progs/unwinder/lua/stack/stack.h @@ -2,9 +2,9 @@ #include "../../metrics.h" -#include "../types.h" #include "../frame.h" #include "../state.h" +#include "../types.h" #include "context.h" @@ -25,35 +25,32 @@ enum lua_stack_step_result { * At this stage frame is known to be useful. Function checks what type of frame it is and pushes info in proper format. * Stack context will be modified to pass filled information about the `lua_frame`. * - * @param context Lua stack context. - * @param frame Current frame. Not NULL. - * @param frame_function `frame_gc(frame)`. + * @param stack_context Stack context. + * @param frame_context Current frame context. * @return `true` if frame was processed successfully, `false` on failure. */ -[[nodiscard]] static ALWAYS_INLINE bool lua_stack_process_frame(struct lua_stack_context* context, const luajit_tvalue* frame, luajit_gc_func* frame_function) { - struct lua_frame* lua_frame = &context->lua_frame; +[[nodiscard]] static ALWAYS_INLINE bool lua_stack_process_frame(struct lua_stack_context* stack_context, struct lua_frame_context* frame_context) { + struct lua_frame* lua_frame = &stack_context->lua_frame; + luajit_gc_func* frame_function = frame_context->frame_function; if (!frame_function) { - LUA_TRACE("[error] lua_stack_process_frame: invalid frame=%px, GCfunc is NULL.", frame); + LUA_LOG_ERROR("Invalid frame=%px, GCfunc is NULL.", frame_context->frame); lua_frame_set_invalid(lua_frame, LUA_FRAME_ERROR_GCFUNC_IS_NULL); return false; } // Probably it's impossible to cover every possible case to always get a valid top frame. // Probably the top frame is invalid, but others below are valid. Continue, mark frame as invalid. - if (!luajit_tvisfunc(frame - LUAJIT_LJ_FR2)) { - LUA_TRACE("[error] lua_stack_process_frame: invalid frame=%px, frame doesn't contain a function, got %d", frame, ~luajit_itype(frame - LUAJIT_LJ_FR2)); + if (!luajit_tvisfunc(frame_context->frame - LUAJIT_LJ_FR2)) { + LUA_LOG_ERROR("Invalid frame=%px, frame doesn't contain a function, got %d", frame_context->frame, ~luajit_itype(frame_context->frame - LUAJIT_LJ_FR2)); lua_frame_set_invalid(lua_frame, LUA_FRAME_ERROR_GCFUNC_WRONG_TYPE); return true; } + LUA_LOG_DEBUG("Processing frame=%px", frame_function); + if (luajit_isluafunc(frame_function)) { - return lua_frame_set_lua( - lua_frame, - &context->symbol_cache_key, - &context->symbol, - frame_function - ); + return lua_frame_set_lua(stack_context, frame_context); } // C and FF functions are handled in the same way @@ -61,31 +58,6 @@ enum lua_stack_step_result { return true; } -/** - * @brief Get next (going top to bottom) frame. - * - * Vararg Lua functions has a special treatment because they take 2 frames. - * First goes Lua frame, then its vararg. - * To save iterations, skip vararg here, take next frame right away. - * - * @see `lj_debug_frame` from LuaJIT source code. - * - * @param frame Current frame. - * @return const luajit_tvalue* Next frame below current. - */ -[[nodiscard]] static ALWAYS_INLINE const luajit_tvalue* lua_stack_next_frame(const luajit_tvalue* frame) { - if (luajit_frame_islua(frame)) { - frame = luajit_frame_prevl(frame); - - // Skip vararg pseudo-frame. (if varg: fallthrough) - if (!luajit_frame_isvarg(frame)) { - return frame; - } - } - - return luajit_frame_prevd(frame); -} - /** * @brief Step the stack once. * @@ -95,36 +67,47 @@ enum lua_stack_step_result { * @return enum lua_stack_step_result Result code. */ [[nodiscard]] NOINLINE enum lua_stack_step_result lua_stack_step() { - struct lua_stack_context* context = lua_stack_context_get(); - if (context == NULL) { - LUA_TRACE("[error] lua_stack_step: failed to get lua_stack_context"); + struct lua_stack_context* stack_context = lua_stack_context_get(); + if (stack_context == NULL) { + LUA_LOG_ERROR("Failed to get lua_stack_context"); return LUA_STACK_STEP_RESULT_STOP; } - const luajit_tvalue* frame = (const luajit_tvalue*)context->frame; - const luajit_tvalue* max_stack = (const luajit_tvalue*)context->max_stack; - const luajit_tvalue* bottom = (const luajit_tvalue*)context->bottom; + luajit_tvalue* frame = (luajit_tvalue*)stack_context->frame; + luajit_tvalue* next_frame = (luajit_tvalue*)stack_context->next_frame; + luajit_tvalue* max_stack = (luajit_tvalue*)stack_context->max_stack; + luajit_tvalue* bottom = (luajit_tvalue*)stack_context->bottom; + // Including NULL if (frame <= bottom) { + LUA_LOG_DEBUG("Frame reached bottom of stack eq=%d", frame == bottom); return LUA_STACK_STEP_RESULT_STOP; } - if (frame >= max_stack) { - LUA_TRACE("[error] lua_stack_step: broken frame"); + LUA_LOG_ERROR("Broken frame=%px (max_stack=%px)", frame, max_stack); return LUA_STACK_STEP_RESULT_STOP; } - context->frame = (u64)lua_stack_next_frame(frame); + // Before leaving the function, update context for next iteration. + // Current frame becomes next, previous becomes current. + // From this point on, use local variables instead of `context`. + lua_stack_context_get_previous_frame(stack_context); - luajit_gc_func* frame_function = (luajit_gc_func*)luajit_frame_gc(frame); + luajit_gc_func* frame_function = luajit_frame_func(frame); // Skip dummy frames. See lj_err_optype_call(). - bool is_dummy_frame = frame_function == (void*)(context->current_lua_state); - if (is_dummy_frame) { + if (frame_function == (void*)(stack_context->lua_state)) { + LUA_LOG_DEBUG("Skipping dummy frame=%px", frame_function); return LUA_STACK_STEP_RESULT_CONTINUE; } - if (!lua_stack_process_frame(context, frame, frame_function)) { + struct lua_frame_context frame_context = { + .frame = frame, + .next_frame = next_frame, + .frame_function = frame_function + }; + + if (!lua_stack_process_frame(stack_context, &frame_context)) { metric_increment(METRIC_LUA_PROCESSED_FRAMES_FAIL_COUNT); return LUA_STACK_STEP_RESULT_STOP | LUA_STACK_STEP_RESULT_HAS_FRAME; } @@ -146,7 +129,7 @@ static ALWAYS_INLINE void lua_stack_reset(struct lua_state* state) { * @brief Add frame to the stack. * * @param state Lua unwind state. - * @param frame Interpreter frame. + * @param lua_frame Interpreter frame. The caller must check remaining capacity. */ static NOINLINE void lua_stack_push(struct lua_state* state, struct lua_frame* lua_frame) { state->stack.len &= LUA_MAX_STACK_DEPTH_VERIFIER_MASK; @@ -169,6 +152,7 @@ static ALWAYS_INLINE void lua_stack_walk(struct lua_state* state) { // VM is idle if (vmstate == ~LUAJIT_VM_STATE_INTERPRETING && !luajit_state_get_cframe(lua_state)) { + LUA_LOG_DEBUG("VM is idle"); return; } @@ -186,32 +170,40 @@ static ALWAYS_INLINE void lua_stack_walk(struct lua_state* state) { const luajit_tvalue* frame = base - 1; - // This might happen if we stopped before full initialize of vararg function frames. - if (luajit_frame_isvarg(frame)) { - frame = luajit_frame_prevd(frame); + if (frame <= bottom || frame >= max_stack) { + LUA_LOG_ERROR("Broken frame bottom=%px max_stack=%px frame=%px", bottom, max_stack, frame); + return; } - if (frame >= max_stack) { - LUA_TRACE("[error] lua_stack_walk: broken frame"); + struct lua_stack_context* stack_context = lua_stack_context_get(); + if (stack_context == NULL) { + LUA_LOG_ERROR("Failed to get lua_stack_context"); return; } - struct lua_stack_context* context = lua_stack_context_get(); - if (context == NULL) { - LUA_TRACE("[error] lua_stack_walk: failed to get lua_stack_context"); - return; + u64 binary_relative_address = state->instruction_pointer - state->binary_start_address; + bool is_in_luajit_vm = state->config.vm_start_pc <= binary_relative_address && binary_relative_address < state->config.vm_end_pc; + u64 bytecode_pc = is_in_luajit_vm ? state->bytecode_pc_register : 0; + + lua_stack_context_init(stack_context, state, frame, max_stack, bottom, bytecode_pc); + + if (!lua_frame_should_skip_vararg(frame)) { + LUA_LOG_DEBUG("Skipping partially initialized vararg function frame."); + lua_stack_context_get_previous_frame(stack_context); } - lua_stack_context_init(context, state, frame, max_stack, bottom); for (int i = 0; i < LUA_MAX_STACK_DEPTH; ++i) { enum lua_stack_step_result status = lua_stack_step(); if (status & LUA_STACK_STEP_RESULT_HAS_FRAME) { - lua_stack_push(state, &context->lua_frame); + lua_stack_push(state, &stack_context->lua_frame); } if (!(status & LUA_STACK_STEP_RESULT_CONTINUE)) { + LUA_LOG_DEBUG("Stopped at frame %d. Frames: %d", i, state->stack.len); return; } } + + LUA_LOG_INFO("Loop reached max stack depth. Frames: %d", state->stack.len); } diff --git a/perforator/agent/collector/progs/unwinder/lua/state.h b/perforator/agent/collector/progs/unwinder/lua/state.h index 64ab91b5e..5a419f5d4 100644 --- a/perforator/agent/collector/progs/unwinder/lua/state.h +++ b/perforator/agent/collector/progs/unwinder/lua/state.h @@ -53,6 +53,7 @@ static ALWAYS_INLINE void lua_state_init_registers(struct lua_state* state, cons state->dispatch_register = user_registers->r14; state->lua_state_register = user_registers->rdi; state->base_register = user_registers->rdx; + state->bytecode_pc_register = user_registers->rbx; } /** @@ -160,14 +161,14 @@ static ALWAYS_INLINE void lua_state_set_lua_state(struct lua_state* state, luaji [[nodiscard]] static ALWAYS_INLINE bool lua_state_test_and_set(struct lua_state* state, luajit_global_state* global_state) { luajit_state* L = luajit_global_state_get_current_lua_state(global_state, &state->config); if (!L) { - LUA_TRACE("[error] lua_state_test_and_set: failed to read global_state->cur_L from global_state=%px", global_state); + LUA_LOG_ERROR("Failed to read global_state->cur_L from global_state=%px", global_state); return false; } // Cross check that `global_state` points to `cur_L`, `cur_L` points to `global_state` luajit_global_state* global_state_from_l = luajit_state_get_glref(L); if (global_state != global_state_from_l) { - LUA_TRACE("[error] lua_state_test_and_set: G(global_state->cur_L)=%px doesn't match global_state=%px", global_state_from_l, global_state); + LUA_LOG_ERROR("G(global_state->cur_L)=%px doesn't match global_state=%px", global_state_from_l, global_state); return false; } @@ -192,14 +193,14 @@ static ALWAYS_INLINE void lua_state_set_lua_state(struct lua_state* state, luaji // Cache exists if (lua_state_test_and_set(state, cached_global_state)) { // L and G are set in `lua_state_test_and_set` - LUA_TRACE("[debug] lua_state_find_g_and_l: using cached G=%px and L=%px", lua_state_get_global_state(state), lua_state_get_lua_state(state)); + LUA_LOG_DEBUG("Using cached G=%px and L=%px", lua_state_get_global_state(state), lua_state_get_lua_state(state)); return true; } // The state is no longer valid, probably called lua_close // Erase cached pointer metric_increment(METRIC_LUA_INVALIDATED_CACHE_COUNT); - LUA_TRACE("[info] lua_state_find_g_and_l: cached G=%px is no longer valid", cached_global_state); + LUA_LOG_INFO("Cached G=%px is no longer valid", cached_global_state); (void)lua_state_cache_set(state, NULL); } @@ -211,7 +212,7 @@ static ALWAYS_INLINE void lua_state_set_lua_state(struct lua_state* state, luaji state->binary_start_address <= state->instruction_pointer && state->instruction_pointer < state->binary_end_address; if (!is_in_luajit_binary) { - LUA_TRACE("[debug] lua_state_find_g_and_l: not in luajit binary %px <= %px < %px", state->binary_start_address, state->instruction_pointer, state->binary_end_address); + LUA_LOG_DEBUG("Not in luajit binary %px <= %px < %px", state->binary_start_address, state->instruction_pointer, state->binary_end_address); return false; } @@ -219,7 +220,7 @@ static ALWAYS_INLINE void lua_state_set_lua_state(struct lua_state* state, luaji // It's always present when we are executing inside LuaJIT VM. luajit_global_state* global_state = luajit_get_global_state_from_dispatch(state->dispatch_register, &state->config); if (lua_state_test_and_set(state, global_state)) { - LUA_TRACE("[info] lua_state_find_g_and_l: found new global_State=%px from dispatch register", global_state); + LUA_LOG_INFO("Found new global_State=%px from dispatch register", global_state); (void)lua_state_cache_set(state, global_state); return true; } @@ -227,12 +228,12 @@ static ALWAYS_INLINE void lua_state_set_lua_state(struct lua_state* state, luaji // Alternatively try to find from register for C ABI first function argument. global_state = luajit_state_get_glref((luajit_state*)state->lua_state_register); if (lua_state_test_and_set(state, global_state)) { - LUA_TRACE("[info] lua_state_find_g_and_l: found new global_State=%px from first argument register", global_state); + LUA_LOG_INFO("Found new global_State=%px from first argument register", global_state); (void)lua_state_cache_set(state, global_state); return true; } - LUA_TRACE("[info] lua_state_find_g_and_l: no valid global_State were found"); + LUA_LOG_INFO("No valid global_State were found"); return false; } @@ -254,17 +255,17 @@ static ALWAYS_INLINE void lua_state_set_lua_state(struct lua_state* state, luaji u64 binary_relative_address = state->instruction_pointer - state->binary_start_address; bool is_in_luajit_vm = state->config.vm_start_pc <= binary_relative_address && binary_relative_address < state->config.vm_end_pc; if (!is_in_luajit_vm) { - LUA_TRACE("[info] lua_state_resolve_base: not in VM, using L->base"); + LUA_LOG_DEBUG("Not in VM, using L->base"); return base; } const luajit_tvalue* base_from_register = (const luajit_tvalue*)state->base_register; if (base_from_register < bottom || base_from_register >= max_stack) { - LUA_TRACE("[info] lua_state_resolve_base: register holding base is below stack, using L->base"); + LUA_LOG_DEBUG("Register holding base is below stack, using L->base"); return base; } - LUA_TRACE("[info] lua_state_resolve_base: using base from the register"); + LUA_LOG_DEBUG("Using base from the register"); return base_from_register; } @@ -279,7 +280,7 @@ static ALWAYS_INLINE void lua_state_set_lua_state(struct lua_state* state, luaji const luajit_tvalue* jit_base = luajit_global_state_get_jit_base(g, &state->config); if (!jit_base) { - LUA_TRACE("[error] lua_state_get_jit_base: failed to read jit_base or it was NULL"); + LUA_LOG_ERROR("Failed to read G->jit_base or it was NULL"); return base; } diff --git a/perforator/agent/collector/progs/unwinder/lua/symbol.h b/perforator/agent/collector/progs/unwinder/lua/symbol.h index 58782651e..46e9085d5 100644 --- a/perforator/agent/collector/progs/unwinder/lua/symbol.h +++ b/perforator/agent/collector/progs/unwinder/lua/symbol.h @@ -4,6 +4,10 @@ // namespace lua::symbol +enum { + LUA_SYMBOL_MAX_FILENAME_LENGTH = 255, // Maximum representable u8 length. +}; + /** * @brief Writes string to symbol buffer. * diff --git a/perforator/agent/collector/progs/unwinder/lua/trace.h b/perforator/agent/collector/progs/unwinder/lua/trace.h index d620aef87..a2ba59647 100644 --- a/perforator/agent/collector/progs/unwinder/lua/trace.h +++ b/perforator/agent/collector/progs/unwinder/lua/trace.h @@ -2,4 +2,27 @@ #include -#define LUA_TRACE(fmt, ...) BPF_TRACE("lua: " fmt "\n", ##__VA_ARGS__) +#define LUA_TRACE_LEVEL(level, fmt, ...) BPF_TRACE("lua: " level " " fmt "\n", ##__VA_ARGS__) + +#define LUA_LOG_FATAL(fmt, ...) LUA_TRACE_LEVEL("[FATAL]", fmt, ##__VA_ARGS__) +#define LUA_LOG_ERROR(fmt, ...) LUA_TRACE_LEVEL("[ERROR]", fmt, ##__VA_ARGS__) +#define LUA_LOG_INFO(fmt, ...) LUA_TRACE_LEVEL("[INFO ]", fmt, ##__VA_ARGS__) +#define LUA_LOG_DEBUG(fmt, ...) LUA_TRACE_LEVEL("[DEBUG]", fmt, ##__VA_ARGS__) + +// Turns on assertions for development purposes. +#ifdef LUA_ENABLE_DEV_ASSERTS + +// Sends SIGSTOP (19) if assertion fails. Attach the debugger afterwards. +#define LUA_DEV_ASSERT(expression, fmt, ...) \ + ({ \ + if (!(expression)) { \ + LUA_LOG_FATAL("Assertion failed: " fmt, ##__VA_ARGS__); \ + bpf_send_signal(19); \ + } \ + }) + +#else // ^^^ LUA_ENABLE_DEV_ASSERTS vvv !LUA_ENABLE_DEV_ASSERTS + +#define LUA_DEV_ASSERT(condition, message) + +#endif // ^^^ !LUA_ENABLE_DEV_ASSERTS diff --git a/perforator/agent/collector/progs/unwinder/lua/types.h b/perforator/agent/collector/progs/unwinder/lua/types.h index 0918c122f..dd00efb6a 100644 --- a/perforator/agent/collector/progs/unwinder/lua/types.h +++ b/perforator/agent/collector/progs/unwinder/lua/types.h @@ -42,7 +42,7 @@ enum lua_frame_error : u8 { /** * @brief Frame in the Lua stack. * - * `value` can be one of three types, depending on `type`: + * `value` depends on `type`: * - `LUA_FRAME_TYPE_LUA` - Lua function. Uses `lua_frame` value. * - `LUA_FRAME_TYPE_C` - C or FastFunction. Uses `c_frame` value. * - `LUA_FRAME_TYPE_INVALID` - Invalid frame. Uses `invalid_frame` value. @@ -50,9 +50,14 @@ enum lua_frame_error : u8 { struct lua_frame { enum lua_frame_type type; union { - struct symbol_key lua_frame; struct { - u64 object_addr; + u64 proto_address; + i32 first_line; + i32 current_line; + u32 bytecode_index; + } lua_frame; + struct { + u64 function_address; u8 ffid; } c_frame; struct { @@ -86,10 +91,11 @@ struct lua_state { u64 binary_end_address; // Last address of LuaJIT binary in memory. // Registers - u64 instruction_pointer; // Value of `rip`. Used to determine if we're executing in LuaJIT binary. - u64 dispatch_register; // Value of `r14`. This register might hold pointer to `GG_State->dispatch`. - u64 lua_state_register; // Value of register for C ABI first function argument. This register might hold pointer to `lua_State`. - u64 base_register; // Value of `rdx`. This register might have a hint about the actual L->base value. + u64 instruction_pointer; // Value of `rip`. Used to determine if we're executing in LuaJIT binary. + u64 dispatch_register; // Value of `r14`. This register might hold pointer to `GG_State->dispatch`. + u64 lua_state_register; // Value of register for C ABI first function argument. This register might hold pointer to `lua_State`. + u64 base_register; // Value of `rdx`. This register might have a hint about the actual L->base value. + u64 bytecode_pc_register; // Value of `rbx`. Inside the interpreter this register holds the bytecode PC. // Main structures u64 current_lua_state; // Current `lua_State*`. Use `lua_state_get_lua_state`. diff --git a/perforator/agent/collector/progs/unwinder/lua/unwind.h b/perforator/agent/collector/progs/unwinder/lua/unwind.h index 55dce34c5..e0ed710a2 100644 --- a/perforator/agent/collector/progs/unwinder/lua/unwind.h +++ b/perforator/agent/collector/progs/unwinder/lua/unwind.h @@ -33,7 +33,9 @@ static ALWAYS_INLINE void lua_collect_stack(const struct process_info* process_i return; } + LUA_LOG_DEBUG(">>> Stack processing start"); lua_stack_walk(state); + LUA_LOG_DEBUG("<<< Stack processing end"); } #endif diff --git a/perforator/internal/linguist/lua/agent/gotest/ya.make b/perforator/internal/linguist/lua/agent/gotest/ya.make new file mode 100644 index 000000000..9251d921d --- /dev/null +++ b/perforator/internal/linguist/lua/agent/gotest/ya.make @@ -0,0 +1,3 @@ +GO_TEST_FOR(perforator/internal/linguist/lua/agent) + +END() diff --git a/perforator/internal/linguist/lua/agent/stackprocessor.go b/perforator/internal/linguist/lua/agent/stackprocessor.go index 8d22a8115..2b1af02a6 100644 --- a/perforator/internal/linguist/lua/agent/stackprocessor.go +++ b/perforator/internal/linguist/lua/agent/stackprocessor.go @@ -1,9 +1,13 @@ package agent import ( + "bytes" + "encoding/binary" "fmt" "strconv" + "github.com/hashicorp/golang-lru/v2/expirable" + "github.com/yandex/perforator/library/go/core/metrics" "github.com/yandex/perforator/perforator/agent/collector/pkg/nativepipeline/runtimewriter" "github.com/yandex/perforator/perforator/agent/collector/pkg/profile" @@ -11,9 +15,10 @@ import ( "github.com/yandex/perforator/perforator/internal/linguist/symbolizer" "github.com/yandex/perforator/perforator/internal/unwinder" "github.com/yandex/perforator/perforator/pkg/linux" + "github.com/yandex/perforator/perforator/pkg/linux/procmem" ) -// Internal frame decoding errors, see lua_stack_walk_error at perforator/agent/collector/progs/unwinder/lua/stack/walk_error.h +// Internal frame decoding errors, see lua_frame_error in perforator/agent/collector/progs/unwinder/lua/types.h var luaStackWalkErrorDescriptions = []string{ "GCfunc is null", "bad function in frame", @@ -28,13 +33,21 @@ type StackProcessor struct { symbolizer *symbolizer.Symbolizer collectedFrameCount metrics.Counter unsymbolizedFrameCount metrics.Counter + collectedNameCount metrics.Counter + unsymbolizedNameCount metrics.Counter + protos *expirable.LRU[luaProtoKey, *luaProto] + strings *expirable.LRU[luaStringKey, string] } -func NewStackProcessor(symbolizer *symbolizer.Symbolizer, reg metrics.Registry) *StackProcessor { +func NewStackProcessor(sym *symbolizer.Symbolizer, reg metrics.Registry) *StackProcessor { return &StackProcessor{ - symbolizer: symbolizer, + symbolizer: sym, collectedFrameCount: reg.Counter("lua.frame.collected.count"), unsymbolizedFrameCount: reg.Counter("lua.frame.unsymbolized.count"), + collectedNameCount: reg.Counter("lua.frame.name.collected.count"), + unsymbolizedNameCount: reg.Counter("lua.frame.name.unsymbolized.count"), + protos: expirable.NewLRU[luaProtoKey, *luaProto](luaProtoCacheCapacity, nil, symbolizer.DefaultCacheTTL), + strings: expirable.NewLRU[luaStringKey, string](luaStringCacheCapacity, nil, symbolizer.DefaultCacheTTL), } } @@ -46,8 +59,14 @@ func (p *StackProcessor) Process( var frames uint32 for i := 0; i < int(stack.Len); i++ { frame := &stack.Frames[i] - p.processFrame(builder, frame, process) + var callerFrame *unwinder.LuaFrame + + // Frames run from callee to caller. The caller's PC names callee's frame. + if i+1 < int(stack.Len) { + callerFrame = &stack.Frames[i+1] + } + p.processFrame(builder, frame, callerFrame, process) frames++ } @@ -57,57 +76,67 @@ func (p *StackProcessor) Process( func (p *StackProcessor) processFrame( builder *profile.SampleBuilder, frame *unwinder.LuaFrame, + callerFrame *unwinder.LuaFrame, process linux.ProcessKey, ) { name := "[lua] " filename := "" - line := int64(0) + firstLine := int64(0) + currentLine := int64(0) clearAddress := false var loc *profile.LocationBuilder switch frame.Type { case unwinder.LuaFrameTypeLua: luaFrame := frame.Value.GetLuaFrame() - symbol, exists := p.symbolizer.Symbolize(models.Language(unwinder.LanguageLua), process, &luaFrame) + foundName := false + symbol, exists := p.symbolizer.Symbolize(models.Language(unwinder.LanguageLua), process, &unwinder.SymbolKey{ObjectAddr: luaFrame.ProtoAddress, Linestart: luaFrame.FirstLine}) if !exists { p.unsymbolizedFrameCount.Inc() - name += fmt.Sprintf("unsymbolized lua function: 0x%x", luaFrame.ObjectAddr) + p.unsymbolizedNameCount.Inc() + name += fmt.Sprintf("unsymbolized lua proto: 0x%x", luaFrame.ProtoAddress) } else { - if len(symbol.Name) == 0 { - name += "" + funcname := p.getFrameName(process, frame, callerFrame, symbol.Name) + foundName = funcname != "" + if !foundName { + p.unsymbolizedNameCount.Inc() + funcname = "" } else { - name += symbol.Name + p.collectedNameCount.Inc() } - - filename = symbol.FileName + name += funcname // Lua prefixes file-backed chunk names with '@'. + filename = symbol.FileName if len(filename) != 0 && filename[0] == '@' { filename = symbol.FileName[1:] } } - line = int64(luaFrame.Linestart) - clearAddress = exists && (symbol.Name != "" || (filename != "" && line > 0)) + firstLine = int64(luaFrame.FirstLine) + currentLine = int64(luaFrame.CurrentLine) + clearAddress = exists && (foundName || (filename != "" && firstLine > 0)) loc = builder.AddInterpreterLocation(&profile.InterpreterLocationKey{ - ObjectAddress: luaFrame.ObjectAddr, - Linestart: luaFrame.Linestart, + ObjectAddress: luaFrame.ProtoAddress, + Linestart: luaFrame.FirstLine, + Line: luaFrame.CurrentLine, Language: models.Language(unwinder.LanguageLua), + Name: name, }) case unwinder.LuaFrameTypeC: cFrame := frame.Value.GetCFrame() if int(cFrame.Ffid) == LuaCFunctionId { - name += fmt.Sprintf("function: 0x%x", cFrame.ObjectAddr) + name += fmt.Sprintf("function: 0x%x", cFrame.FunctionAddress) } else { name += "function: builtin#" + strconv.Itoa(int(cFrame.Ffid)) clearAddress = true } loc = builder.AddInterpreterLocation(&profile.InterpreterLocationKey{ - ObjectAddress: cFrame.ObjectAddr, + ObjectAddress: cFrame.FunctionAddress, Linestart: int32(cFrame.Ffid), Language: models.Language(unwinder.LanguageLua), }) @@ -129,36 +158,45 @@ func (p *StackProcessor) processFrame( if clearAddress { loc.ClearAddress() } + loc.SetMapping().SetPath(string(profile.LuaSpecialMapping)).Finish() loc.AddFrame(). SetName(name). SetFilename(filename). - SetLine(line). - SetStartLine(line). + SetLine(currentLine). + SetStartLine(firstLine). Finish() loc.Finish() } -func (p *StackProcessor) ResolveFrame(process linux.ProcessKey, frame *unwinder.LuaFrame, output runtimewriter.Frame) error { +func (p *StackProcessor) ResolveFrame(process linux.ProcessKey, frame *unwinder.LuaFrame, callerFrame *unwinder.LuaFrame, output runtimewriter.Frame) error { name := "[lua] " filename := "" - line := int64(0) + firstLine := int64(0) + currentLine := int64(0) clearAddress := false var address uint64 switch frame.Type { case unwinder.LuaFrameTypeLua: luaFrame := frame.Value.GetLuaFrame() - symbol, exists := p.symbolizer.Symbolize(models.Language(unwinder.LanguageLua), process, &luaFrame) + foundName := false + symbol, exists := p.symbolizer.Symbolize(models.Language(unwinder.LanguageLua), process, &unwinder.SymbolKey{ObjectAddr: luaFrame.ProtoAddress, Linestart: luaFrame.FirstLine}) if !exists { - name += fmt.Sprintf("unsymbolized lua function: 0x%x", luaFrame.ObjectAddr) + p.unsymbolizedFrameCount.Inc() + p.unsymbolizedNameCount.Inc() + name += fmt.Sprintf("unsymbolized lua proto: 0x%x", luaFrame.ProtoAddress) } else { - if len(symbol.Name) == 0 { - name += "" + funcname := p.getFrameName(process, frame, callerFrame, symbol.Name) + foundName = funcname != "" + if !foundName { + p.unsymbolizedNameCount.Inc() + funcname = "" } else { - name += symbol.Name + p.collectedNameCount.Inc() } + name += funcname filename = symbol.FileName @@ -168,21 +206,22 @@ func (p *StackProcessor) ResolveFrame(process linux.ProcessKey, frame *unwinder. } } - line = int64(luaFrame.Linestart) - clearAddress = exists && (symbol.Name != "" || (filename != "" && line > 0)) + address = luaFrame.ProtoAddress + firstLine = int64(luaFrame.FirstLine) + currentLine = int64(luaFrame.CurrentLine) - address = luaFrame.ObjectAddr + clearAddress = exists && (foundName || (filename != "" && firstLine > 0)) case unwinder.LuaFrameTypeC: cFrame := frame.Value.GetCFrame() if int(cFrame.Ffid) == LuaCFunctionId { - name += fmt.Sprintf("function: 0x%x", cFrame.ObjectAddr) + name += fmt.Sprintf("function: 0x%x", cFrame.FunctionAddress) } else { name += "function: builtin#" + strconv.Itoa(int(cFrame.Ffid)) clearAddress = true } - address = cFrame.ObjectAddr + address = cFrame.FunctionAddress case unwinder.LuaFrameTypeInvalid: invalidFrame := frame.Value.GetInvalidFrame() name += "" @@ -199,9 +238,464 @@ func (p *StackProcessor) ResolveFrame(process linux.ProcessKey, frame *unwinder. if err := output.SetMappingName(profile.LuaSpecialMapping); err != nil { return err } + if clearAddress { address = 0 } + output.SetAddress(address) - return output.AddInlineFunction(name, filename, int32(line), int32(line)) + return output.AddInlineFunction(name, filename, int32(firstLine), int32(currentLine)) +} + +// Agent read/cache budgets, not LuaJIT's maximum allocation sizes. +const ( + luaProtoReadLimit = 1 << 20 + luaStringReadLimit = 1 << 10 + luaProtoCacheCapacity = 256 + luaStringCacheCapacity = 1024 +) + +// LuaJIT layout for LJ_GC64=1, LJ_FR2=1, x86-64 (lj_obj.h). +const ( + luaProtoSize = 104 + luaGCStrSize = 24 + luaGCRefSize = 8 + luaBCInsSize = 4 + luaTValueSize = 8 + luaGCPointerBits = 47 + luaGCMask = 1<> luaBCAShift) & luaBCRegMax +} + +func luaBCOperandC(instruction uint32) uint32 { + return (instruction >> luaBCCShift) & luaBCRegMax +} + +func luaBCOperandD(instruction uint32) uint32 { + return instruction >> luaBCDShift +} + +const ( + luaBCMov = 18 + luaBCKnil = 44 + luaBCUget = 45 + luaBCGget = 54 + luaBCTgets = 57 + luaBCIterc = 69 + luaBCModeDst = 1 + luaBCModeBase = 2 + luaVarNameEnd = 0 + luaVarNameMax = 7 + + // Name representation of MM_call and MM__MAX; an absent metamethod has no name. + luaMMCall = "__call" + luaMMMax = "" +) + +// BCDEF metadata for A operand in opcode order. Only destination/base writes are retained. +var luaBCModeAArray = [...]uint8{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, // 0..15 + 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 16..31 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 0, 0, // 32..47 + 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 2, // 48..63 + 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, // 64..79 + 2, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, // 80..95 +} + +// BCDEF metadata for metamethod kind in opcode order. +var luaBCModeMetamethodArray = [...]string{ + "__lt", "__lt", "__le", "__le", "__eq", "__eq", "__eq", "__eq", // 0..7 + "__eq", "__eq", "__eq", "__eq", luaMMMax, luaMMMax, luaMMMax, luaMMMax, // 8..15 + luaMMMax, luaMMMax, luaMMMax, luaMMMax, "__unm", "__len", "__add", "__sub", // 16..23 + "__mul", "__div", "__mod", "__add", "__sub", "__mul", "__div", "__mod", // 24..31 + "__add", "__sub", "__mul", "__div", "__mod", "__pow", "__concat", luaMMMax, // 32..39 + luaMMMax, luaMMMax, luaMMMax, luaMMMax, luaMMMax, luaMMMax, luaMMMax, luaMMMax, // 40..47 + luaMMMax, luaMMMax, luaMMMax, "__gc", "__gc", "__gc", "__index", "__newindex", // 48..55 + "__index", "__index", "__index", "__index", "__newindex", "__newindex", "__newindex", "__newindex", // 56..63 + "__newindex", luaMMCall, luaMMCall, luaMMCall, luaMMCall, luaMMCall, luaMMCall, // 64..70 +} + +func luaBCModeA(opcode uint8) uint8 { + return luaBCModeAArray[opcode] +} + +func luaBCModeMetamethod(opcode uint8) string { + return luaBCModeMetamethodArray[opcode] +} + +var luaVarNames = [luaVarNameMax]string{ + "", "(for index)", "(for limit)", "(for step)", "(for generator)", "(for state)", "(for control)", +} + +type luaProtoKey struct { + process linux.ProcessKey + address uint64 + firstLine int32 +} + +type luaStringKey struct { + process linux.ProcessKey + proto *luaProto + address uint64 +} + +// GCproto fields used for name lookup, decoded from the eBPF symbol cache. +type luaProtoFields struct { + gct uint8 + framesize uint8 + sizebc uint64 + k uint64 + sizekgc uint64 + sizept uint64 + sizeuv uint8 + firstline int32 + numline uint32 + uvinfo uint64 + varinfo uint64 +} + +func decodeLuaProtoFields(data []byte) luaProtoFields { + return luaProtoFields{ + gct: data[luaGCHeaderGCTOffset], + framesize: data[luaProtoFrameSizeOffset], + sizebc: uint64(binary.LittleEndian.Uint32(data[luaProtoSizeBCOffset:])), + k: binary.LittleEndian.Uint64(data[luaProtoKOffset:]), + sizekgc: uint64(binary.LittleEndian.Uint32(data[luaProtoSizeKGCOffset:])), + sizept: uint64(binary.LittleEndian.Uint32(data[luaProtoSizePTOffset:])), + sizeuv: data[luaProtoSizeUVOffset], + firstline: int32(binary.LittleEndian.Uint32(data[luaProtoFirstLineOffset:])), + numline: binary.LittleEndian.Uint32(data[luaProtoNumLineOffset:]), + uvinfo: binary.LittleEndian.Uint64(data[luaProtoUVInfoOffset:]), + varinfo: binary.LittleEndian.Uint64(data[luaProtoVarInfoOffset:]), + } +} + +// All GCproto arrays are colocated in this immutable snapshot. String constants +// live outside the proto and are cached separately, after being requested. +type luaProto struct { + header luaProtoFields + bytecode []byte + kgc []byte + uvinfo []byte + varinfo []byte +} + +func (p *StackProcessor) readLuaProtoFields(key luaProtoKey, rawStruct string) *luaProto { + if len(rawStruct) != luaProtoSize { + return nil + } + + if proto, ok := p.protos.Get(key); ok { + return proto + } + + if key.address == 0 || key.address > luaGCMask-luaProtoSize { + return nil + } + + protoStruct := decodeLuaProtoFields([]byte(rawStruct)) + if protoStruct.gct != luaGCTProto || protoStruct.firstline != key.firstLine || + protoStruct.sizept < luaProtoSize || protoStruct.sizept > luaProtoReadLimit || protoStruct.sizept > luaGCMask-key.address || + protoStruct.sizebc == 0 || protoStruct.sizebc*luaBCInsSize > protoStruct.sizept-luaProtoSize { + return nil + } + + data := make([]byte, protoStruct.sizept) + copy(data, rawStruct) + if procmem.Read(int(key.process.Pid), uintptr(key.address+luaProtoSize), data[luaProtoSize:]) != nil { + return nil + } + + // MRef fields must point inside the proto allocation (or be absent). + field := func(addr uint64) []byte { + if addr < key.address+luaProtoSize || addr >= key.address+protoStruct.sizept { + return nil + } + return data[addr-key.address:] + } + + if protoStruct.k < key.address+luaProtoSize+protoStruct.sizebc*luaBCInsSize+protoStruct.sizekgc*luaGCRefSize || protoStruct.k > key.address+protoStruct.sizept { + return nil + } + proto := &luaProto{ + header: protoStruct, + bytecode: data[luaProtoSize : luaProtoSize+protoStruct.sizebc*luaBCInsSize], + kgc: data[protoStruct.k-key.address-protoStruct.sizekgc*luaGCRefSize : protoStruct.k-key.address], + uvinfo: field(protoStruct.uvinfo), + varinfo: field(protoStruct.varinfo), + } + + // Upvalue names end where the local-variable debug data starts. + if len(proto.uvinfo) >= len(proto.varinfo) && len(proto.varinfo) != 0 { + proto.uvinfo = proto.uvinfo[:len(proto.uvinfo)-len(proto.varinfo)] + } + + p.protos.Add(key, proto) + return proto +} + +// See `lj_debug_funcname` in LuaJIT source code +func (p *StackProcessor) getFrameName(process linux.ProcessKey, currentFrame *unwinder.LuaFrame, previousFrame *unwinder.LuaFrame, protoRaw string) string { + currentLuaFrame := currentFrame.Value.GetLuaFrame() + + if len(protoRaw) != luaProtoSize { + return "" + } + header := decodeLuaProtoFields([]byte(protoRaw)) + if header.gct != luaGCTProto || header.firstline != currentLuaFrame.FirstLine { + return "" + } + + if currentLuaFrame.FirstLine == 0 && header.numline != 0 { + return "in main chunk" + } + + if previousFrame == nil || previousFrame.Type != unwinder.LuaFrameTypeLua { + // TODO: calling function through pcall/xpcall loses function name, but there is a way to get it + return "" + } + + previousLuaFrame := previousFrame.Value.GetLuaFrame() + if previousLuaFrame.BytecodeIndex == luaBCNoPosition { + return "" + } + + previousProtoRaw := protoRaw + if previousLuaFrame.ProtoAddress != currentLuaFrame.ProtoAddress || previousLuaFrame.FirstLine != currentLuaFrame.FirstLine { + callerSymbol, exists := p.symbolizer.Symbolize(models.Language(unwinder.LanguageLua), process, &unwinder.SymbolKey{ObjectAddr: previousLuaFrame.ProtoAddress, Linestart: previousLuaFrame.FirstLine}) + if !exists { + return "" + } + previousProtoRaw = callerSymbol.Name + } + previousFrameProto := p.readLuaProtoFields(luaProtoKey{process, previousLuaFrame.ProtoAddress, previousLuaFrame.FirstLine}, previousProtoRaw) + if previousFrameProto == nil || uint64(previousLuaFrame.BytecodeIndex)*luaBCInsSize >= uint64(len(previousFrameProto.bytecode)) { + return "" + } + + instruction := previousFrameProto.instruction(previousLuaFrame.BytecodeIndex) + opcode := luaBCOpcode(instruction) + if int(opcode) >= len(luaBCModeMetamethodArray) { + return "" + } + + // Metamethod + metamethod := luaBCModeMetamethod(opcode) + if metamethod != luaMMCall { + return metamethod + } + + // Regular call + slot := luaBCOperandA(instruction) + if opcode == luaBCIterc { + if slot < luaIteratorControlSlots { + return "" + } + slot -= luaIteratorControlSlots + } + + return p.getSlotName(process, previousFrameProto, previousLuaFrame.BytecodeIndex, slot) +} + +// See `lj_debug_slotname` in LuaJIT source code +func (p *StackProcessor) getSlotName(process linux.ProcessKey, proto *luaProto, bytecodeIndex uint32, slot uint32) string { +restart: + if name := proto.variableName(bytecodeIndex, slot); name != "" { + return name + } + + for bytecodeIndex > luaBCFirstInstruction { + bytecodeIndex-- + + instruction := proto.instruction(bytecodeIndex) + opcode, a := luaBCOpcode(instruction), luaBCOperandA(instruction) + if int(opcode) >= len(luaBCModeAArray) { + return "" + } + + if luaBCModeA(opcode) == luaBCModeBase { + if slot >= a && (opcode != luaBCKnil || slot <= luaBCOperandD(instruction)) { + return "" + } + } else if luaBCModeA(opcode) == luaBCModeDst && a == slot { + switch opcode { + case luaBCMov: + slot = luaBCOperandD(instruction) + + // MOV instruction is in AD format, but D can't be larger than proto framesize + if slot >= uint32(proto.header.framesize) { + return "" + } + + goto restart + case luaBCGget: + return p.getConstant(process, proto, luaBCOperandD(instruction)) + case luaBCTgets: + return p.getConstant(process, proto, luaBCOperandC(instruction)) + case luaBCUget: + return proto.getUpvalueName(luaBCOperandD(instruction)) + default: + return "" + } + } + } + + return "" +} + +// `proto_kgc(pt, ~idx)` with caching +func (p *StackProcessor) getConstant(process linux.ProcessKey, proto *luaProto, index uint32) string { + // proto_kgc(pt, ~idx): collectable constants precede k, in reverse order. + if uint64(index) >= uint64(len(proto.kgc))/luaGCRefSize { + return "" + } + + address := binary.LittleEndian.Uint64(proto.kgc[uint64(len(proto.kgc))-(uint64(index)+1)*luaGCRefSize:]) + + key := luaStringKey{process, proto, address} + if name, ok := p.strings.Get(key); ok { + return name + } + + if address == 0 || address > luaGCMask-luaGCStrSize { + return "" + } + + // GCstr, followed by its payload. + var gcStr [luaGCStrSize]byte + if procmem.Read(int(process.Pid), uintptr(address), gcStr[:]) != nil || gcStr[luaGCHeaderGCTOffset] != luaGCTStr { + return "" + } + + length := binary.LittleEndian.Uint32(gcStr[luaGCStrLengthOffset:]) + if length > luaStringReadLimit || uint64(length) > luaGCMask-address-luaGCStrSize { + return "" + } + + data := make([]byte, length) + if procmem.Read(int(process.Pid), uintptr(address+luaGCStrSize), data) != nil { + return "" + } + + // LuaJIT exposes names as C strings, even if a string constant contains NUL. + if end := bytes.IndexByte(data, 0); end >= 0 { + data = data[:end] + } + + name := string(data) + p.strings.Add(key, name) + return name +} + +func (proto *luaProto) instruction(bytecodeIndex uint32) uint32 { + return binary.LittleEndian.Uint32(proto.bytecode[uint64(bytecodeIndex)*luaBCInsSize:]) +} + +// See `lj_debug_uvname` in LuaJIT soucre code +func (proto *luaProto) getUpvalueName(index uint32) string { + names := proto.uvinfo + + if index >= uint32(proto.header.sizeuv) { + return "" + } + + for i := uint32(0); i <= index; i++ { + end := bytes.IndexByte(names, 0) + if end < 0 { + return "" + } + + if i == index { + return string(names[:end]) + } + + names = names[end+1:] + } + + return "" +} + +// See `debug_varname` in LuaJIT source code +func (proto *luaProto) variableName(bytecodeIndex uint32, slot uint32) string { + variableInfo, lastpc := proto.varinfo, uint64(0) + + for len(variableInfo) != 0 && variableInfo[0] != luaVarNameEnd { + vn := variableInfo[0] + var name []byte + + if int(vn) < luaVarNameMax { + variableInfo = variableInfo[1:] + } else { + end := bytes.IndexByte(variableInfo, 0) + if end < 0 { + return "" + } + + name, variableInfo = variableInfo[:end], variableInfo[end+1:] + } + + delta, n := binary.Uvarint(variableInfo) + if n <= 0 || delta > uint64(^uint32(0))-lastpc { + return "" + } + + lastpc += delta + if lastpc > uint64(bytecodeIndex) { + break + } + + variableInfo = variableInfo[n:] + extent, n := binary.Uvarint(variableInfo) + if n <= 0 || extent > uint64(^uint32(0))-lastpc { + return "" + } + variableInfo = variableInfo[n:] + + if uint64(bytecodeIndex) < lastpc+extent { + if slot == 0 { + if int(vn) < luaVarNameMax { + return luaVarNames[vn] + } + return string(name) + } + slot-- + } + } + + return "" } diff --git a/perforator/internal/linguist/lua/agent/stackprocessor_test.go b/perforator/internal/linguist/lua/agent/stackprocessor_test.go new file mode 100644 index 000000000..51aa52d36 --- /dev/null +++ b/perforator/internal/linguist/lua/agent/stackprocessor_test.go @@ -0,0 +1,412 @@ +package agent + +import ( + "encoding/binary" + "fmt" + "os" + "runtime" + "strings" + "testing" + "unsafe" + + "github.com/yandex/perforator/library/go/core/metrics/nop" + "github.com/yandex/perforator/perforator/agent/collector/pkg/profile" + "github.com/yandex/perforator/perforator/internal/linguist/models" + "github.com/yandex/perforator/perforator/internal/linguist/symbolizer" + "github.com/yandex/perforator/perforator/internal/unwinder" + "github.com/yandex/perforator/perforator/pkg/linux" +) + +// Offsets within the unwinder's Lua frame union (unwinder/lua/types.h). +const ( + testLuaFrameFirstLineOffset = 8 + testLuaFramePCOffset = 16 +) + +// Frame size and additional opcode values used only for regression fixtures. +const ( + testLuaFrameSize = 4 + testLuaBCCall = 66 + testLuaBCFnew = 51 + testLuaBCKshort = 41 + testLuaBCAddvv = 32 + testLuaBCIseqv = 4 +) + +func luaAD(op, a, d uint32) uint32 { return op | a< tc.maxAllocs { + t.Fatalf("got %g allocations, want at most %g", allocs, tc.maxAllocs) + } + }) + } +} + +func TestLuaMainChunkName(t *testing.T) { + process := linux.ProcessKey{Pid: linux.CurrentNamespacePID(os.Getpid()), ProcessStartTime: 1} + for _, tc := range []struct { + name string + firstline uint32 + numline uint32 + caller bool + want string + }{ + {"main without caller", 0, 2, false, "in main chunk"}, + {"main with caller", 0, 2, true, "in main chunk"}, + {"regular function", 10, 2, true, "calledfn"}, + {"regular without lines", 10, 0, true, "calledfn"}, + {"stripped function", 0, 0, true, "calledfn"}, + {"stripped without caller", 0, 0, false, ""}, + } { + t.Run(tc.name, func(t *testing.T) { + p, source := newLuaNameTestProcessor(t) + data, _, current := luaNameFixture([]uint32{0, 0}, nil, nil) + binary.LittleEndian.PutUint32(data[luaProtoFirstLineOffset:], tc.firstline) + binary.LittleEndian.PutUint32(data[luaProtoNumLineOffset:], tc.numline) + binary.LittleEndian.PutUint32(current.Value.UnionBuf[testLuaFrameFirstLineOffset:], tc.firstline) + // Main-chunk naming does not require a valid current PC. + binary.LittleEndian.PutUint32(current.Value.UnionBuf[testLuaFramePCOffset:], luaBCNoPosition) + callerData, strings, caller := luaNameFixture([]uint32{0, luaAD(luaBCGget, 0, 0), luaAD(testLuaBCCall, 0, 0)}, nil, nil, "calledfn") + source.add(process, data[:luaProtoSize], current) + source.add(process, callerData[:luaProtoSize], caller) + if !tc.caller { + caller = nil + } + if got := luaTestFrameName(p, process, current, caller); got != tc.want { + t.Fatalf("got %q, want %q", got, tc.want) + } + runtime.KeepAlive(data) + runtime.KeepAlive(callerData) + runtime.KeepAlive(strings) + }) + } +} + +func TestLuaProtoRequiresCapturedHeader(t *testing.T) { + process := linux.ProcessKey{Pid: linux.CurrentNamespacePID(os.Getpid())} + data, _, frame := luaNameFixture([]uint32{0, luaAD(testLuaBCAddvv, 0, 0)}, nil, nil) + for _, length := range []int{0, luaProtoSize - 1, luaProtoSize + 1} { + p, source := newLuaNameTestProcessor(t) + if length != 0 { + source.add(process, data[:length], frame) + } + if got := luaTestFrameName(p, process, frame, frame); got != "" { + t.Fatalf("header length %d: read proto from memory instead of rejecting symbol: %q", length, got) + } + } + runtime.KeepAlive(data) +} + +func TestLuaProcessFrameSkipsUnsymbolizedName(t *testing.T) { + process := linux.ProcessKey{Pid: linux.CurrentNamespacePID(os.Getpid())} + p, source := newLuaNameTestProcessor(t) + data, _, current := luaNameFixture([]uint32{0, 0}, nil, nil) + callerData, strings, caller := luaNameFixture([]uint32{0, luaAD(luaBCGget, 0, 0), luaAD(testLuaBCCall, 0, 0)}, nil, nil, "calledfn") + source.add(process, callerData[:luaProtoSize], caller) + // A valid caller can name the function, but its own symbol is missing. + b := profile.NewBuilder().AddSampleType("cpu", "cycles") + sample := b.Add(process).AddValue(1) + p.processFrame(sample, current, caller, process) + sample.Finish() + result := b.FinishRaw() + want := fmt.Sprintf("[lua] (unsymbolized lua proto: 0x%x)", current.Value.GetLuaFrame().ProtoAddress) + if got := result.Location[0].Line[0].Function.Name; got != want { + t.Fatalf("got %q, want %q", got, want) + } + if p.protos.Len() != 0 || p.strings.Len() != 0 { + t.Fatal("unsymbolized frame attempted function-name lookup") + } + runtime.KeepAlive(data) + runtime.KeepAlive(callerData) + runtime.KeepAlive(strings) +} + +func TestLuaFrameAddressClearing(t *testing.T) { + process := linux.ProcessKey{Pid: linux.CurrentNamespacePID(os.Getpid())} + for _, tc := range []struct { + name string + firstline uint32 + numline uint32 + filename string + clear bool + }{ + {"main chunk", 0, 2, "", true}, + {"unnamed function", 10, 2, "", false}, + {"unnamed function with source", 10, 2, "@script.lua", true}, + {"stripped function", 0, 0, "", false}, + {"stripped function with filename", 0, 0, "@script.lua", false}, + } { + t.Run(tc.name, func(t *testing.T) { + p, source := newLuaNameTestProcessor(t) + data, _, frame := luaNameFixture([]uint32{0, 0}, nil, nil) + binary.LittleEndian.PutUint32(data[luaProtoFirstLineOffset:], tc.firstline) + binary.LittleEndian.PutUint32(data[luaProtoNumLineOffset:], tc.numline) + binary.LittleEndian.PutUint32(frame.Value.UnionBuf[testLuaFrameFirstLineOffset:], tc.firstline) + source.add(process, data[:luaProtoSize], frame) + for key, symbol := range source { + symbol.FilenameLength = uint8(len(tc.filename)) + copy(symbol.Data[luaProtoSize:], tc.filename) + source[key] = symbol + } + b := profile.NewBuilder().AddSampleType("cpu", "cycles") + sample := b.Add(process).AddValue(1) + p.processFrame(sample, frame, nil, process) + sample.Finish() + want := frame.Value.GetLuaFrame().ProtoAddress + if tc.clear { + want = 0 + } + if got := b.FinishRaw().Location[0].Address; got != want { + t.Fatalf("address = %#x, want %#x", got, want) + } + runtime.KeepAlive(data) + }) + } +} + +func TestLuaLocationsPreserveCallerNames(t *testing.T) { + b := profile.NewBuilder().AddSampleType("cpu", "cycles") + for _, name := range []string{"first", "alias", "first"} { + sample := b.Add(linux.ProcessKey{Pid: 1}).AddValue(1) + loc := sample.AddInterpreterLocation(&profile.InterpreterLocationKey{ + ObjectAddress: 0xabc, + Linestart: 10, + Line: 12, + Language: models.Language(unwinder.LanguageLua), + Name: name, + }) + loc.AddFrame().SetName(name).Finish() + loc.Finish().Finish() + } + p := b.FinishRaw() + if len(p.Location) != 2 || p.Sample[0].Location[0] != p.Sample[2].Location[0] || p.Sample[1].Location[0].Line[0].Function.Name != "alias" { + t.Fatal("locations merged different caller names or failed to deduplicate the same name") + } +} diff --git a/perforator/internal/linguist/lua/agent/ya.make b/perforator/internal/linguist/lua/agent/ya.make index db881c6a1..f7de1ed5a 100644 --- a/perforator/internal/linguist/lua/agent/ya.make +++ b/perforator/internal/linguist/lua/agent/ya.make @@ -5,4 +5,8 @@ SRCS( stackprocessor.go ) +GO_TEST_SRCS(stackprocessor_test.go) + END() + +RECURSE_FOR_TESTS(gotest) diff --git a/perforator/internal/linguist/symbolizer/symbolizer.go b/perforator/internal/linguist/symbolizer/symbolizer.go index 2bf697993..611af58a8 100644 --- a/perforator/internal/linguist/symbolizer/symbolizer.go +++ b/perforator/internal/linguist/symbolizer/symbolizer.go @@ -73,7 +73,7 @@ func NewPhpSymbolizer(c *SymbolizerConfig, state *programstate.State, reg metric return newSymbolizer(c, state, reg, "php") } -func NewLuaSymbolizer(c *SymbolizerConfig, state *programstate.State, reg metrics.Registry) (*Symbolizer, error) { +func NewLuaSymbolizer(c *SymbolizerConfig, state symbolSource, reg metrics.Registry) (*Symbolizer, error) { return newSymbolizer(c, state, reg, "lua") } @@ -237,7 +237,11 @@ func (s *Symbolizer) symbolize(language models.Language, process linux.ProcessKe switch symbol.CodepointSize { case 1: - name = copy.ZeroTerminatedString(nameBytes) + if language == models.Language(unwinder.LanguageLua) { + name = string(nameBytes) + } else { + name = copy.ZeroTerminatedString(nameBytes) + } fileName = copy.ZeroTerminatedString(filenameBytes) case 2: name = s.decodeUTF16(nameBytes) diff --git a/perforator/internal/linguist/symbolizer/symbolizer_test.go b/perforator/internal/linguist/symbolizer/symbolizer_test.go index acb64eaef..4eed1c835 100644 --- a/perforator/internal/linguist/symbolizer/symbolizer_test.go +++ b/perforator/internal/linguist/symbolizer/symbolizer_test.go @@ -195,3 +195,27 @@ func TestSymbolCacheRetriesMissingSymbols(t *testing.T) { } require.Equal(t, 2, source.calls) } + +func TestLuaSymbolPreservesProtoHeader(t *testing.T) { + const protoSize = 104 // sizeof(GCproto), LJ_GC64=1. + key := unwinder.InterpreterSymbolKey{ + SymbolKey: unwinder.SymbolKey{ObjectAddr: 0x1000, Linestart: 10}, + Pid: 42, Language: uint8(unwinder.LanguageLua), ProcessStarttime: 100, + } + filename := strings.Repeat("f", 255) + raw := unwinder.Symbol{CodepointSize: 1, NameLength: protoSize, FilenameLength: uint8(len(filename))} + for i := range raw.Data[:protoSize] { + raw.Data[i] = byte(i) // Includes NUL and binary bytes, not a function name. + } + copy(raw.Data[protoSize:], filename) + source := &fakeSymbolSource{symbols: map[unwinder.InterpreterSymbolKey]unwinder.Symbol{key: raw}} + s, err := newSymbolizer(&SymbolizerConfig{}, source, nop.Registry{}, "lua") + require.NoError(t, err) + for i := 0; i < 2; i++ { + symbol, ok := s.Symbolize(models.Language(key.Language), linux.ProcessKey{Pid: linux.CurrentNamespacePID(key.Pid), ProcessStartTime: key.ProcessStarttime}, &key.SymbolKey) + require.True(t, ok) + require.Equal(t, string(raw.Data[:protoSize]), symbol.Name) + require.Equal(t, filename, symbol.FileName) + } + require.Equal(t, 1, source.calls) +} diff --git a/perforator/internal/unwinder/sample_parser_test.go b/perforator/internal/unwinder/sample_parser_test.go index 6932583ba..66a62da79 100644 --- a/perforator/internal/unwinder/sample_parser_test.go +++ b/perforator/internal/unwinder/sample_parser_test.go @@ -53,8 +53,8 @@ func TestLanguageFrameSizes(t *testing.T) { if phpFrameSize != 16 { t.Errorf("php_frame size = %d, want 16", phpFrameSize) } - if luaFrameSize != 24 { - t.Errorf("lua_frame size = %d, want 24", luaFrameSize) + if luaFrameSize != 32 { + t.Errorf("lua_frame size = %d, want 32", luaFrameSize) } } @@ -205,12 +205,14 @@ func appendPhpFrame(dst []byte, objectAddr uint64, linestart int32) []byte { // appendLuaFrame appends a lua_frame with type LUA_FRAME_TYPE_LUA. Layout: // -// u8 type | u64 object_addr | i32 linestart | u32 padding. -func appendLuaFrame(dst []byte, objectAddr uint64, linestart int32) []byte { +// u8 type | padding[7] | u64 proto_address | i32 first_line | i32 current_line | u32 bytecode_index | padding[4]. +func appendLuaFrame(dst []byte, protoAddress uint64, firstLine, currentLine int32, bytecodeIndex uint32) []byte { frame := make([]byte, luaFrameSize) - le.PutUint64(frame[0:8], uint64(LuaFrameTypeLua)) - le.PutUint64(frame[8:16], objectAddr) - le.PutUint32(frame[16:20], uint32(linestart)) + frame[0] = byte(LuaFrameTypeLua) + le.PutUint64(frame[8:16], protoAddress) + le.PutUint32(frame[16:20], uint32(firstLine)) + le.PutUint32(frame[20:24], uint32(currentLine)) + le.PutUint32(frame[24:28], bytecodeIndex) return append(dst, frame...) } @@ -294,7 +296,7 @@ func TestParsePackedSampleWithPhpStack(t *testing.T) { func TestParsePackedSampleWithLuaStack(t *testing.T) { data := buildMinimalPackedSample() - frames := appendLuaFrame(nil, 0x1a2b3c, 77) + frames := appendLuaFrame(nil, 0x1a2b3c, 77, 81, 12) langData := appendLanguageSection(nil, LanguageLua, frames) putSectionDesc(data, sdLangSect, 0, uint16(len(langData))) data = append(data, langData...) @@ -308,8 +310,9 @@ func TestParsePackedSampleWithLuaStack(t *testing.T) { t.Fatalf("LuaStack.Len = %d, want 1", out.LuaStack.Len) } got := out.LuaStack.Frames[0] - if got.Type != LuaFrameTypeLua || got.Value.GetLuaFrame().ObjectAddr != 0x1a2b3c || - got.Value.GetLuaFrame().Linestart != 77 { + lua := got.Value.GetLuaFrame() + if got.Type != LuaFrameTypeLua || lua.ProtoAddress != 0x1a2b3c || + lua.FirstLine != 77 || lua.CurrentLine != 81 || lua.BytecodeIndex != 12 { t.Errorf("frame = %+v", got) } } @@ -322,7 +325,7 @@ func buildPackedSampleWithMixedLanguageStacks() []byte { le.PutUint16(jvmFrames[0:2], 3) le.PutUint64(jvmFrames[8:16], 0x3000) - luaFrames := appendLuaFrame(nil, 0x4000, 404) + luaFrames := appendLuaFrame(nil, 0x4000, 404, 405, 9) var langData []byte langData = appendLanguageSection(langData, LanguagePython, pythonFrames) @@ -350,7 +353,9 @@ func TestParsePackedSampleWithMixedLanguageStacks(t *testing.T) { if out.JvmStack.FramesLen != 1 || out.JvmStack.Frames[0].MethodAddr != 0x3000 { t.Errorf("JvmStack = %+v", out.JvmStack) } - if out.LuaStack.Len != 1 || out.LuaStack.Frames[0].Value.GetLuaFrame().ObjectAddr != 0x4000 { + lua := out.LuaStack.Frames[0].Value.GetLuaFrame() + if out.LuaStack.Len != 1 || lua.ProtoAddress != 0x4000 || + lua.FirstLine != 404 || lua.CurrentLine != 405 || lua.BytecodeIndex != 9 { t.Errorf("LuaStack = %+v", out.LuaStack) } } diff --git a/perforator/internal/unwinder/wire_fixtures_test.go b/perforator/internal/unwinder/wire_fixtures_test.go index 73db15353..08195da98 100644 --- a/perforator/internal/unwinder/wire_fixtures_test.go +++ b/perforator/internal/unwinder/wire_fixtures_test.go @@ -7,14 +7,14 @@ import ( ) func TestWireFormat(t *testing.T) { - // The same little-endian record and mutations are checked in raw_sample_ut.cpp. + // Cases mirror raw_sample_ut.cpp, using the current Lua frame layout here. // Header, kernel/user stacks, LBR, TLS, cgroups, then Python/PHP/JVM/Lua. const recordHex = "0000000001000000000000000000000000000000000000000000000000000000" + "01000300e8030000393000000000000000000000000000000000000000000000" + "000000000000000000000000000000002a0000002b0000000000000000000000" + "00000000000000002b0200000000000064000000000000000100000000000000" + "0100000000000000c80000000000000000001000100008001800300048003001" + - "7801080080017800100000000000000020000000000000003000000000000000" + + "7801080080018000100000000000000020000000000000003000000000000000" + "4000000000000000500000000000000007000000000000006000000000000000" + "7000000000000000090000000000000040000000000000000100000000000000" + "6300000000000000000000000000000000000000000000000000000000000000" + @@ -29,7 +29,9 @@ func TestWireFormat(t *testing.T) { "2a00000000000000200000012000000034120000000000000b00000000000000" + "3812000000000000785600000000000010000101100000004523000000000000" + "0c00000000000000100002021000000000000000000000005634000000000000" + - "1800030118000000000000000000000067450000000000000d00000000000000" + "2000030120000000000000000000000067450000000000000d00000011000000" + + "0500000000000000" + const recordSize = 680 base, err := hex.DecodeString(recordHex) if err != nil { t.Fatal(err) @@ -41,31 +43,31 @@ func TestWireFormat(t *testing.T) { width int accept bool }{ - {"all_sections", 672, 0, 0, 0, true}, - {"perf_padding", 676, 0, 0, 0, true}, + {"all_sections", recordSize, 0, 0, 0, true}, + {"perf_padding", recordSize + 4, 0, 0, 0, true}, {"short_header", 167, 0, 0, 0, false}, - {"truncated_payload", 671, 0, 0, 0, false}, - {"wrong_tag", 672, 0, 1, 1, false}, - {"undefined_sample_type", 672, 4, 0, 1, false}, - {"unknown_sample_type", 672, 4, 6, 1, false}, - {"overlapping_sections", 672, 148, 8, 1, false}, - {"gap_between_sections", 672, 148, 24, 1, false}, - {"partial_stack", 672, 146, 15, 1, false}, - {"misaligned_section", 672, 144, 1, 1, false}, - {"section_out_of_bounds", 672, 166, 65535, 2, false}, - {"unknown_language", 672, 554, 4, 1, false}, - {"reserved_language_byte_zero", 672, 555, 0, 1, true}, - {"zero_element_size", 672, 556, 0, 1, false}, - {"misaligned_element_size", 672, 556, 7, 1, false}, - {"zero_payload_size", 672, 552, 0, 1, false}, - {"partial_language_element", 672, 552, 31, 1, false}, - {"truncated_language_header", 672, 166, 1, 1, false}, - {"language_out_of_bounds", 672, 552, 4096, 2, false}, - {"duplicate_language", 672, 594, 0, 1, false}, - {"reserved_language_byte_nonzero", 672, 555, 2, 1, true}, + {"truncated_payload", recordSize - 1, 0, 0, 0, false}, + {"wrong_tag", recordSize, 0, 1, 1, false}, + {"undefined_sample_type", recordSize, 4, 0, 1, false}, + {"unknown_sample_type", recordSize, 4, 6, 1, false}, + {"overlapping_sections", recordSize, 148, 8, 1, false}, + {"gap_between_sections", recordSize, 148, 24, 1, false}, + {"partial_stack", recordSize, 146, 15, 1, false}, + {"misaligned_section", recordSize, 144, 1, 1, false}, + {"section_out_of_bounds", recordSize, 166, 65535, 2, false}, + {"unknown_language", recordSize, 554, 4, 1, false}, + {"reserved_language_byte_zero", recordSize, 555, 0, 1, true}, + {"zero_element_size", recordSize, 556, 0, 1, false}, + {"misaligned_element_size", recordSize, 556, 7, 1, false}, + {"zero_payload_size", recordSize, 552, 0, 1, false}, + {"partial_language_element", recordSize, 552, 31, 1, false}, + {"truncated_language_header", recordSize, 166, 1, 1, false}, + {"language_out_of_bounds", recordSize, 552, 4096, 2, false}, + {"duplicate_language", recordSize, 594, 0, 1, false}, + {"reserved_language_byte_nonzero", recordSize, 555, 2, 1, true}, // Go requires known frame layouts; C++ keeps these interpreter payloads opaque. - {"opaque_interpreter_size", 672, 556, 16, 1, false}, - {"reserved_jvm_byte_nonzero", 672, 619, 1, 1, true}, + {"opaque_interpreter_size", recordSize, 556, 16, 1, false}, + {"reserved_jvm_byte_nonzero", recordSize, 619, 1, 1, true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -90,7 +92,11 @@ func TestWireFormat(t *testing.T) { out.PythonStack.Frames[0].SymbolKey.Linestart != 11 || out.PythonStack.Frames[0].InstrPtr != 0x1238 || out.PhpStack.Frames[0].SymbolKey.ObjectAddr != 0x2345 || out.PhpStack.Frames[0].SymbolKey.Linestart != 12 || out.JvmStack.Frames[0].MethodAddr != 0x3456 || out.LuaStack.Len != 1 || - out.LuaStack.Frames[0].Value.GetLuaFrame().Linestart != 13 { + out.LuaStack.Frames[0].Type != LuaFrameTypeLua || + out.LuaStack.Frames[0].Value.GetLuaFrame().ProtoAddress != 0x4567 || + out.LuaStack.Frames[0].Value.GetLuaFrame().FirstLine != 13 || + out.LuaStack.Frames[0].Value.GetLuaFrame().CurrentLine != 17 || + out.LuaStack.Frames[0].Value.GetLuaFrame().BytecodeIndex != 5 { t.Fatalf("unexpected decoded fixture: %+v", out) } if len(out.LBR) != 2 || len(out.TLS) != 2 {