9#define LLVM_SYMBOLIZER_BIN "llvm-symbolizer.exe"
10#define ADDR2LINE_BIN "addr2line.exe"
14#define LLVM_SYMBOLIZER_BIN "llvm-symbolizer"
15#define ADDR2LINE_BIN "addr2line"
43#define NULL_SENTINEL "[NULL]"
56static atomic_bool g_symbolizer_detected =
false;
57static atomic_bool g_llvm_symbolizer_checked =
false;
58static atomic_bool g_llvm_symbolizer_available =
false;
60static atomic_bool g_addr2line_checked =
false;
61static atomic_bool g_addr2line_available =
false;
115static rwlock_t g_symbol_cache_lock = {0};
116static atomic_bool g_symbol_cache_initialized =
false;
119static atomic_uint_fast64_t g_cache_hits = 0;
120static atomic_uint_fast64_t g_cache_misses = 0;
130static bool symbolizer_path_is_executable(
const char *path) {
132 if (!path || path[0] ==
'\0') {
136 DWORD attrs = GetFileAttributesA(path);
137 if (attrs == INVALID_FILE_ATTRIBUTES) {
140 if (attrs & FILE_ATTRIBUTE_DIRECTORY) {
144 HANDLE handle = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
145 FILE_ATTRIBUTE_NORMAL, NULL);
146 if (handle == INVALID_HANDLE_VALUE) {
153 return (path && path[0] !=
'\0' && access(path, X_OK) == 0);
157static const char *get_llvm_symbolizer_command(
void) {
158 if (!atomic_load(&g_llvm_symbolizer_checked)) {
159 static_mutex_lock(&g_symbolizer_detection_mutex);
160 if (!atomic_load(&g_llvm_symbolizer_checked)) {
161 const char *env_path =
SAFE_GETENV(
"LLVM_SYMBOLIZER_PATH");
162 bool available =
false;
164 g_llvm_symbolizer_cmd[0] =
'\0';
166 if (env_path && env_path[0] !=
'\0') {
167 if (symbolizer_path_is_executable(env_path)) {
168 SAFE_STRNCPY(g_llvm_symbolizer_cmd, env_path,
sizeof(g_llvm_symbolizer_cmd));
170 log_debug(
"Using llvm-symbolizer from LLVM_SYMBOLIZER_PATH: %s", env_path);
172 log_warn(
"LLVM_SYMBOLIZER_PATH is set but not executable: %s", env_path);
178 g_llvm_symbolizer_cmd[0] =
'\0';
182 atomic_store(&g_llvm_symbolizer_available, available);
183 atomic_store(&g_llvm_symbolizer_checked,
true);
185 static_mutex_unlock(&g_symbolizer_detection_mutex);
188 if (!atomic_load(&g_llvm_symbolizer_available)) {
192 if (g_llvm_symbolizer_cmd[0] !=
'\0') {
193 return g_llvm_symbolizer_cmd;
199static const char *get_addr2line_command(
void) {
200 if (!atomic_load(&g_addr2line_checked)) {
201 static_mutex_lock(&g_symbolizer_detection_mutex);
202 if (!atomic_load(&g_addr2line_checked)) {
203 const char *env_path =
SAFE_GETENV(
"ADDR2LINE_PATH");
204 bool available =
false;
206 g_addr2line_cmd[0] =
'\0';
208 if (env_path && env_path[0] !=
'\0') {
209 if (symbolizer_path_is_executable(env_path)) {
210 SAFE_STRNCPY(g_addr2line_cmd, env_path,
sizeof(g_addr2line_cmd));
212 log_debug(
"Using addr2line from ADDR2LINE_PATH: %s", env_path);
214 log_warn(
"ADDR2LINE_PATH is set but not executable: %s", env_path);
220 g_addr2line_cmd[0] =
'\0';
224 atomic_store(&g_addr2line_available, available);
225 atomic_store(&g_addr2line_checked,
true);
227 static_mutex_unlock(&g_symbolizer_detection_mutex);
230 if (!atomic_load(&g_addr2line_available)) {
234 if (g_addr2line_cmd[0] !=
'\0') {
235 return g_addr2line_cmd;
242#include <mach-o/dyld.h>
257static bool get_macos_file_offset(
const void *addr,
char *out_path,
size_t path_size, uintptr_t *out_file_offset) {
258 if (!addr || !out_path || !out_file_offset) {
262 uintptr_t target_addr = (uintptr_t)addr;
263 uint32_t image_count = _dyld_image_count();
265 for (
uint32_t i = 0; i < image_count; i++) {
266 const struct mach_header *header = _dyld_get_image_header(i);
271 intptr_t slide = _dyld_get_image_vmaddr_slide(i);
272 const char *image_name = _dyld_get_image_name(i);
278 if (header->magic == MH_MAGIC_64) {
279 const struct mach_header_64 *header64 = (
const struct mach_header_64 *)header;
282 for (
uint32_t j = 0; j < header64->ncmds; j++) {
283 const struct load_command *cmd = (
const struct load_command *)ptr;
285 if (cmd->cmd == LC_SEGMENT_64) {
286 const struct segment_command_64 *seg = (
const struct segment_command_64 *)ptr;
287 uintptr_t seg_start = seg->vmaddr + (uintptr_t)slide;
288 uintptr_t seg_end = seg_start + seg->vmsize;
290 if (target_addr >= seg_start && target_addr < seg_end) {
293 *out_file_offset = target_addr - (uintptr_t)slide;
305#elif defined(__linux__)
319static bool get_linux_file_offset(
const void *addr,
char *out_path,
size_t path_size, uintptr_t *out_file_offset) {
320 if (!addr || !out_path || !out_file_offset) {
324 uintptr_t target_addr = (uintptr_t)addr;
327 FILE *maps = fopen(
"/proc/self/maps",
"r");
335 while (fgets(line,
sizeof(line), maps)) {
337 uintptr_t start_addr, end_addr, file_offset;
342 int matched = sscanf(line,
"%lx-%lx %4s %lx %*x:%*x %*d %511s", &start_addr, &end_addr, perms, &file_offset, path);
346 if (perms[2] ==
'x' && target_addr >= start_addr && target_addr < end_addr) {
348 if (strstr(path,
"ascii-chat") != NULL && strstr(path,
".so") == NULL) {
350 *out_file_offset = (target_addr - start_addr) + file_offset;
354 log_debug(
"ASLR: addr=%p -> file_offset=0x%lx (segment_base=0x%lx, segment_file_offset=0x%lx, path=%s)", addr,
355 *out_file_offset, start_addr, file_offset, path);
371 const char *llvm_symbolizer = get_llvm_symbolizer_command();
372 if (llvm_symbolizer) {
373 log_debug(
"Using llvm-symbolizer for symbol resolution");
377 const char *addr2line_cmd = get_addr2line_command();
379 log_debug(
"Using addr2line command: %s", addr2line_cmd);
392 bool expected =
false;
393 if (!atomic_compare_exchange_strong(&g_symbol_cache_initialized, &expected,
true)) {
399 if (atomic_compare_exchange_strong(&g_symbolizer_detected, &expected,
true)) {
400 g_symbolizer_type = detect_symbolizer();
405 atomic_store(&g_symbol_cache_initialized,
false);
410 g_symbol_cache = NULL;
412 atomic_store(&g_cache_hits, 0);
413 atomic_store(&g_cache_misses, 0);
420 if (!atomic_load(&g_symbol_cache_initialized)) {
425 atomic_store(&g_symbol_cache_initialized,
false);
431 size_t entry_count = HASH_COUNT(g_symbol_cache);
435 size_t freed_count = 0;
436 HASH_ITER(hh, g_symbol_cache, entry, tmp) {
438 HASH_DEL(g_symbol_cache, entry);
453 g_symbol_cache = NULL;
455 log_debug(
"Symbol cache cleaned up: %zu entries counted, %zu entries freed (hits=%llu, misses=%llu)", entry_count,
456 freed_count, (
unsigned long long)atomic_load(&g_cache_hits),
457 (
unsigned long long)atomic_load(&g_cache_misses));
461 if (!atomic_load(&g_symbol_cache_initialized) || !addr) {
468 HASH_FIND_PTR(g_symbol_cache, &addr, entry);
471 const char *symbol = entry->
symbol;
472 atomic_fetch_add(&g_cache_hits, 1);
477 atomic_fetch_add(&g_cache_misses, 1);
483 if (!atomic_load(&g_symbol_cache_initialized) || !addr || !symbol) {
492 if (!atomic_load(&g_symbol_cache_initialized)) {
499 HASH_FIND_PTR(g_symbol_cache, &addr, existing);
503 if (existing->
symbol && strcmp(existing->
symbol, symbol) != 0) {
532 HASH_ADD_PTR(g_symbol_cache, addr, entry);
542 *hits_out = atomic_load(&g_cache_hits);
545 *misses_out = atomic_load(&g_cache_misses);
549 *entries_out = HASH_COUNT(g_symbol_cache);
555 uint64_t hits = atomic_load(&g_cache_hits);
556 uint64_t misses = atomic_load(&g_cache_misses);
559 size_t entries = HASH_COUNT(g_symbol_cache);
563 double hit_rate = total > 0 ? (100.0 * (double)hits / (
double)total) : 0.0;
565 log_info(
"Symbol Cache Stats: %zu entries, %llu hits, %llu misses (%.1f%% hit rate)", entries,
566 (
unsigned long long)hits, (
unsigned long long)misses, hit_rate);
579static char *parse_llvm_symbolizer_result(FILE *fp,
void *addr) {
580 char func_name[512] =
"??";
581 char file_location[512] =
"??:0";
585 if (fgets(func_name,
sizeof(func_name), fp) == NULL) {
588 func_name[strcspn(func_name,
"\n")] =
'\0';
591 if (fgets(file_location,
sizeof(file_location), fp) == NULL) {
594 file_location[strcspn(file_location,
"\n")] =
'\0';
597 if (fgets(blank_line,
sizeof(blank_line), fp) == NULL) {
602 char *last_colon = strrchr(file_location,
':');
622 bool has_func = (strcmp(func_name,
"??") != 0 && strlen(func_name) > 0);
624 (strcmp(file_location,
"??:0") != 0 && strcmp(file_location,
"??:?") != 0 && strcmp(file_location,
"??") != 0);
627 char clean_func[512];
628 SAFE_STRNCPY(clean_func, func_name,
sizeof(clean_func));
629 char *paren = strstr(clean_func,
"()");
634 if (!has_func && !has_file) {
636 }
else if (has_func && has_file) {
637 SAFE_SNPRINTF(result, 1024,
"%s() (%s)", clean_func, rel_path);
638 }
else if (has_func) {
641 SAFE_SNPRINTF(result, 1024,
"%s (unknown function)", rel_path);
656static char **run_llvm_symbolizer_batch(
void *
const *buffer,
int size) {
657 if (size <= 0 || !buffer) {
661 const char *symbolizer_cmd = get_llvm_symbolizer_command();
662 if (!symbolizer_cmd) {
663 log_debug(
"llvm-symbolizer not available - skipping symbolization");
668 char **result =
SAFE_CALLOC((
size_t)(size + 1),
sizeof(
char *),
char **);
680 uintptr_t file_offsets[64];
681 int original_indices[64];
685 binary_group_t groups[8];
688 for (
int i = 0; i < size; i++) {
690 uintptr_t file_offset = 0;
692 if (!get_macos_file_offset(buffer[i], binary_path,
sizeof(binary_path), &file_offset)) {
703 for (
int g = 0; g < num_groups; g++) {
704 if (strcmp(groups[g].binary_path, binary_path) == 0) {
710 if (group_idx < 0 && num_groups < 8) {
711 group_idx = num_groups++;
712 SAFE_STRNCPY(groups[group_idx].binary_path, binary_path,
sizeof(groups[group_idx].binary_path));
713 groups[group_idx].count = 0;
716 if (group_idx >= 0 && groups[group_idx].count < 64) {
717 int idx = groups[group_idx].count++;
718 groups[group_idx].file_offsets[idx] = file_offset;
719 groups[group_idx].original_indices[idx] = i;
724 for (
int g = 0; g < num_groups; g++) {
725 if (groups[g].count == 0) {
731 int offset = snprintf(cmd,
sizeof(cmd),
"%s --demangle --output-style=LLVM --relativenames -e '%s' ",
732 symbolizer_cmd, groups[g].binary_path);
734 for (
int j = 0; j < groups[g].count && offset < (int)
sizeof(cmd) - 32; j++) {
735 int n = snprintf(cmd + offset,
sizeof(cmd) - (
size_t)offset,
"0x%lx ", (
unsigned long)groups[g].file_offsets[j]);
742 strncat(cmd,
"2>/dev/null",
sizeof(cmd) - strlen(cmd) - 1);
744 FILE *fp = popen(cmd,
"r");
750 for (
int j = 0; j < groups[g].count; j++) {
751 int orig_idx = groups[g].original_indices[j];
752 result[orig_idx] = parse_llvm_symbolizer_result(fp, buffer[orig_idx]);
753 if (!result[orig_idx]) {
755 if (result[orig_idx]) {
764#elif defined(__linux__)
769 uintptr_t file_offsets[64];
770 int original_indices[64];
774 binary_group_t groups[8];
777 for (
int i = 0; i < size; i++) {
779 uintptr_t file_offset = 0;
781 if (!get_linux_file_offset(buffer[i], binary_path,
sizeof(binary_path), &file_offset)) {
792 for (
int g = 0; g < num_groups; g++) {
793 if (strcmp(groups[g].binary_path, binary_path) == 0) {
799 if (group_idx < 0 && num_groups < 8) {
800 group_idx = num_groups++;
801 SAFE_STRNCPY(groups[group_idx].binary_path, binary_path,
sizeof(groups[group_idx].binary_path));
802 groups[group_idx].count = 0;
805 if (group_idx >= 0 && groups[group_idx].count < 64) {
806 int idx = groups[group_idx].count++;
807 groups[group_idx].file_offsets[idx] = file_offset;
808 groups[group_idx].original_indices[idx] = i;
813 for (
int g = 0; g < num_groups; g++) {
814 if (groups[g].count == 0) {
820 if (!
escape_path_for_shell(groups[g].binary_path, escaped_binary_path,
sizeof(escaped_binary_path))) {
826 int offset = snprintf(cmd,
sizeof(cmd),
"%s --demangle --output-style=LLVM --relativenames -e %s ", symbolizer_cmd,
827 escaped_binary_path);
829 for (
int j = 0; j < groups[g].count && offset < (int)
sizeof(cmd) - 32; j++) {
830 int n = snprintf(cmd + offset,
sizeof(cmd) - (
size_t)offset,
"0x%lx ", (
unsigned long)groups[g].file_offsets[j]);
837 strncat(cmd,
"2>/dev/null",
sizeof(cmd) - strlen(cmd) - 1);
839 FILE *fp = popen(cmd,
"r");
845 for (
int j = 0; j < groups[g].count; j++) {
846 int orig_idx = groups[g].original_indices[j];
847 result[orig_idx] = parse_llvm_symbolizer_result(fp, buffer[orig_idx]);
848 if (!result[orig_idx]) {
850 if (result[orig_idx]) {
882 int offset = snprintf(cmd,
sizeof(cmd),
"%s --demangle --output-style=LLVM --relativenames -e %s ",
883 escaped_symbolizer, escaped_exe_path);
885 for (
int i = 0; i < size && offset < (int)
sizeof(cmd) - 32; i++) {
886 int n = snprintf(cmd + offset,
sizeof(cmd) - (
size_t)offset,
"0x%llx ", (
unsigned long long)buffer[i]);
892 FILE *fp = popen(cmd,
"r");
898 for (
int i = 0; i < size; i++) {
899 result[i] = parse_llvm_symbolizer_result(fp, buffer[i]);
920static char **run_addr2line_batch(
void *
const *buffer,
int size) {
921 if (size <= 0 || !buffer) {
922 log_error(
"Invalid parameters: buffer=%p, size=%d", (
void *)buffer, size);
926 const char *addr2line_cmd = get_addr2line_command();
927 if (!addr2line_cmd) {
928 log_debug(
"addr2line not available - skipping symbolization");
941 log_error(
"Invalid executable path - contains unsafe characters: %s", exe_path);
948 log_error(
"Failed to escape executable path for shell command");
951 const char *escaped_exe_path = escaped_exe_path_buf;
954 log_warn(
"addr2line path contains unsafe characters: %s", addr2line_cmd);
961 log_error(
"Failed to escape addr2line path for shell command");
964 const char *escaped_addr2line_cmd = escaped_addr2line_buf;
968 int offset = snprintf(cmd,
sizeof(cmd),
"%s -e %s -f -C -i ", escaped_addr2line_cmd, escaped_exe_path);
969 if (offset <= 0 || offset >= (
int)
sizeof(cmd)) {
970 log_error(
"Failed to build addr2line command");
975 for (
int i = 0; i < size; i++) {
976 int n = snprintf(cmd + offset,
sizeof(cmd) - (
size_t)offset,
"0x%llx ", (
unsigned long long)buffer[i]);
977 if (n <= 0 || offset + n >= (
int)
sizeof(cmd)) {
978 log_error(
"Failed to build addr2line command");
985 FILE *fp = popen(cmd,
"r");
987 log_error(
"Failed to execute addr2line command");
992 char **result =
SAFE_CALLOC((
size_t)(size + 1),
sizeof(
char *),
char **);
999 for (
int i = 0; i < size; i++) {
1000 char func_name[256];
1001 char file_line[512];
1003 if (fgets(func_name,
sizeof(func_name), fp) == NULL) {
1006 if (fgets(file_line,
sizeof(file_line), fp) == NULL) {
1011 func_name[strcspn(func_name,
"\n")] =
'\0';
1012 file_line[strcspn(file_line,
"\n")] =
'\0';
1020 SAFE_STRNCPY(rel_path, rel_path_tmp,
sizeof(rel_path));
1026 bool has_func = (strcmp(func_name,
"??") != 0);
1027 bool has_file = (strcmp(file_line,
"??:0") != 0 && strcmp(file_line,
"??:?") != 0);
1029 if (!has_func && !has_file) {
1032 }
else if (has_func && has_file) {
1034 if (strstr(rel_path,
":") != NULL) {
1035 SAFE_SNPRINTF(result[i], 1024,
"%s in %s()", rel_path, func_name);
1037 SAFE_SNPRINTF(result[i], 1024,
"%s() at %s", func_name, rel_path);
1039 }
else if (has_func) {
1041 SAFE_SNPRINTF(result[i], 1024,
"%s() at %p", func_name, buffer[i]);
1044 SAFE_SNPRINTF(result[i], 1024,
"%s (unknown function)", rel_path);
1053 if (size <= 0 || !buffer) {
1054 log_error(
"Invalid parameters: buffer=%p, size=%d", (
void *)buffer, size);
1060 if (!atomic_load(&g_symbol_cache_initialized)) {
1063 char **result = run_llvm_symbolizer_batch(buffer, size);
1065 result = run_addr2line_batch(buffer, size);
1072 char **result =
SAFE_CALLOC((
size_t)(size + 1),
sizeof(
char *),
char **);
1078 result[size] = NULL;
1081 int uncached_count = 0;
1082 void *uncached_addrs[size];
1083 int uncached_indices[size];
1085 for (
int i = 0; i < size; i++) {
1096 uncached_addrs[uncached_count] = buffer[i];
1097 uncached_indices[uncached_count] = i;
1103 if (uncached_count > 0) {
1104 char **resolved = NULL;
1107 switch (g_symbolizer_type) {
1109 resolved = run_llvm_symbolizer_batch(uncached_addrs, uncached_count);
1112 resolved = run_addr2line_batch(uncached_addrs, uncached_count);
1122 for (
int i = 0; i < uncached_count; i++) {
1123 int orig_idx = uncached_indices[i];
1127 if (!result[orig_idx]) {
1128 log_error(
"Failed to duplicate string for result[%d]", orig_idx);
1132 if (result[orig_idx] && strcmp(result[orig_idx],
NULL_SENTINEL) != 0) {
1134 log_error(
"Failed to insert symbol into cache for result[%d]", orig_idx);
1140 if (!result[orig_idx]) {
1144 if (!result[orig_idx]) {
1145 log_error(
"Failed to allocate memory for result[%d]", orig_idx);
1153 for (
int i = 0; i < uncached_count; i++) {
1154 int orig_idx = uncached_indices[i];
1155 if (!result[orig_idx]) {
1157 if (result[orig_idx]) {
1158 SAFE_SNPRINTF(result[orig_idx], 32,
"%p", uncached_addrs[i]);
1163 if (!result[orig_idx]) {
1164 log_error(
"Failed to allocate memory for result[%d]", orig_idx);
1185 for (
int i = 0; i < 64; i++) {
1186 if (symbols[i] == NULL) {
#define SAFE_STRNCPY(dst, src, size)
#define SAFE_MALLOC(size, cast)
#define SAFE_GETENV(name)
#define SAFE_SNPRINTF(buffer, buffer_size,...)
#define SAFE_CALLOC(count, size, cast)
#define PLATFORM_MAX_PATH_LENGTH
unsigned long long uint64_t
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
#define log_warn(...)
Log a WARN message.
#define log_error(...)
Log an ERROR message.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
bool validate_shell_safe(const char *str, const char *allowed_chars)
Validate that a string contains only safe characters for shell commands.
bool escape_path_for_shell(const char *path, char *out_buffer, size_t out_buffer_size)
Escape a path for safe use in shell commands (auto-platform)
const char * extract_project_relative_path(const char *file)
Extract relative path from an absolute path.
Platform initialization and static synchronization helpers.
📂 Path Manipulation Utilities
Cross-platform read-write lock interface for ascii-chat.
Static mutex structure for global mutexes requiring static initialization.
Symbol cache entry structure for address-to-symbol mapping.
UT_hash_handle hh
uthash handle (required for hash table operations)
char * symbol
Resolved symbol string (allocated, owned by cache)
void * addr
Memory address key (used for hashtable lookup)
#define LLVM_SYMBOLIZER_BIN
Symbol Resolution Cache for Backtrace Addresses.
Cross-platform system functions interface for ascii-chat.
#️⃣ Wrapper for uthash.h that ensures common.h is included first
🔤 String Manipulation and Shell Escaping Utilities