Skip to content

Commit 0106c32

Browse files
authored
[k2] add backtrace_symbols (#1332)
add analogue of `backtrace_symbols` from <execinfo.h> to capture debug info if it present
1 parent 178ca0a commit 0106c32

7 files changed

Lines changed: 238 additions & 16 deletions

File tree

runtime-light/k2-platform/k2-header.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ int32_t k2_code_segment_offset(uint64_t* offset);
458458
* `ENODATA` => there is no debug information for the image
459459
* `EFAULT` => attempt to dereference a nullptr
460460
*/
461-
int32_t k2_symbol_name_len(const void* addr, size_t* name_len);
461+
int32_t k2_symbol_name_len(void* addr, size_t* name_len);
462462

463463
/**
464464
* Return symbol filename's len
@@ -470,7 +470,7 @@ int32_t k2_symbol_name_len(const void* addr, size_t* name_len);
470470
* `ENODATA` => there is no debug information for the image
471471
* `EFAULT` => attempt to dereference a nullptr
472472
*/
473-
int32_t k2_symbol_filename_len(const void* addr, size_t* filename_len);
473+
int32_t k2_symbol_filename_len(void* addr, size_t* filename_len);
474474

475475
struct SymbolInfo {
476476
char* name;
@@ -489,7 +489,7 @@ struct SymbolInfo {
489489
* `ENODATA` => there is no debug information for the image
490490
* `EFAULT` => attempt to dereference a nullptr
491491
*/
492-
int32_t k2_resolve_symbol(const void* addr, struct SymbolInfo* symbol_info);
492+
int32_t k2_resolve_symbol(void* addr, struct SymbolInfo* symbol_info);
493493

494494
// ---- libc analogues, designed to work instance-local ----
495495

runtime-light/stdlib/diagnostics/backtrace.h

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,35 @@
66

77
#include <cstddef>
88
#include <cstdint>
9+
#include <expected>
910
#include <ranges>
1011
#include <span>
12+
#include <utility>
1113

1214
#include "runtime-light/k2-platform/k2-api.h"
1315

1416
namespace kphp::diagnostic {
1517

1618
size_t backtrace(std::span<void*> addresses) noexcept;
1719

18-
inline auto backtrace_code_addresses(std::span<void* const> addresses) noexcept {
19-
static constexpr std::span<void* const> empty_span{};
20+
inline auto backtrace_addresses(std::span<void* const> addresses) noexcept {
2021
uint64_t code_segment_offset{};
2122
auto error_code{k2::code_segment_offset(&code_segment_offset)};
2223
auto address_transform{[code_segment_offset](void* address) noexcept { return static_cast<void*>(static_cast<std::byte*>(address) - code_segment_offset); }};
2324
if (error_code != k2::errno_ok) [[unlikely]] {
24-
return std::views::transform(empty_span, address_transform);
25+
return std::views::transform(std::span<void* const>{}, address_transform);
2526
}
2627
return addresses | std::views::transform(address_transform);
2728
}
2829

30+
/**
31+
* `backtrace_symbol`s stops resolving `addresses` when it finds the first address that cannot be resolved.
32+
* As a result, the length of the output may be non-zero but less than the length of the input `addresses`.
33+
* */
34+
inline auto backtrace_symbols(std::span<void* const> addresses) noexcept {
35+
return addresses | std::views::transform([](void* addr) noexcept { return k2::resolve_symbol(addr); }) |
36+
std::views::take_while([](const auto& value) noexcept { return value.has_value(); }) |
37+
std::views::transform([](std::expected<k2::SymbolInfo, int32_t>&& value) noexcept { return std::move(value).value(); });
38+
}
39+
2940
} // namespace kphp::diagnostic

runtime-light/stdlib/diagnostics/error-handling-functions.h

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,73 @@
99
#include <format>
1010

1111
#include "runtime-common/core/runtime-core.h"
12+
#include "runtime-light/k2-platform/k2-api.h"
1213
#include "runtime-light/stdlib/diagnostics/backtrace.h"
1314
#include "runtime-light/utils/logs.h"
1415

15-
inline array<array<string>> f$debug_backtrace() noexcept {
16-
static constexpr size_t MAX_STACKTRACE_DEPTH = 64;
16+
namespace error_handling_impl_ {
17+
18+
inline array<array<string>> format_backtrace_symbols(std::span<void* const> backtrace) noexcept {
19+
auto resolved_backtrace{kphp::diagnostic::backtrace_symbols(backtrace)};
20+
if (resolved_backtrace.empty()) {
21+
return {};
22+
}
23+
24+
array<array<string>> backtrace_symbols{array_size{static_cast<int64_t>(backtrace.size()), true}};
25+
const string function_key{"function"};
26+
const string filename_key{"file"};
27+
const string line_key{"line"};
28+
29+
for (const k2::SymbolInfo& symbol_info : resolved_backtrace) {
30+
array<string> frame_info{array_size{3, false}};
31+
frame_info.set_value(function_key, string{symbol_info.name.get()});
32+
frame_info.set_value(filename_key, string{symbol_info.filename.get()});
33+
frame_info.set_value(line_key, string{static_cast<int64_t>(symbol_info.lineno)});
34+
35+
backtrace_symbols.emplace_back(std::move(frame_info));
36+
}
37+
38+
return backtrace_symbols;
39+
}
40+
41+
inline array<array<string>> format_backtrace_addresses(std::span<void* const> backtrace) noexcept {
1742
static constexpr size_t LOG_BUFFER_SIZE = 32;
1843

19-
std::array<void*, MAX_STACKTRACE_DEPTH> raw_trace{};
20-
size_t num_frames{kphp::diagnostic::backtrace(raw_trace)};
21-
auto resolved_backtrace{kphp::diagnostic::backtrace_code_addresses(std::span<void*>(raw_trace.data(), num_frames))};
22-
if (resolved_backtrace.empty()) [[unlikely]] {
23-
kphp::log::warning("cannot resolve virtual addresses to static offsets");
44+
auto resolved_backtrace{kphp::diagnostic::backtrace_addresses(backtrace)};
45+
if (resolved_backtrace.empty()) {
2446
return {};
2547
}
2648

27-
array<array<string>> backtrace{array_size{static_cast<int64_t>(num_frames), true}};
49+
array<array<string>> backtrace_addresses{array_size{static_cast<int64_t>(backtrace.size()), true}};
2850
const string function_key{"function"};
2951

3052
for (const auto& address : resolved_backtrace) {
3153
std::array<char, LOG_BUFFER_SIZE> log_buffer{};
3254
const auto [_, recorded]{std::format_to_n(log_buffer.data(), log_buffer.size() - 1, "{}", address)};
3355
array<string> frame_info{array_size{1, false}};
3456
frame_info.set_value(function_key, string{log_buffer.data(), static_cast<string::size_type>(recorded)});
35-
backtrace.emplace_back(std::move(frame_info));
57+
58+
backtrace_addresses.emplace_back(std::move(frame_info));
3659
}
3760

38-
return backtrace;
61+
return backtrace_addresses;
62+
}
63+
} // namespace error_handling_impl_
64+
65+
inline array<array<string>> f$debug_backtrace() noexcept {
66+
static constexpr size_t MAX_STACKTRACE_DEPTH = 64;
67+
68+
std::array<void*, MAX_STACKTRACE_DEPTH> backtrace{};
69+
size_t num_frames{kphp::diagnostic::backtrace(backtrace)};
70+
std::span<void*> backtrace_view{backtrace.data(), num_frames};
71+
auto backtrace_symbols{error_handling_impl_::format_backtrace_symbols(backtrace_view)};
72+
if (!backtrace_symbols.empty()) {
73+
return backtrace_symbols;
74+
}
75+
auto backtrace_code_addresses{error_handling_impl_::format_backtrace_addresses(backtrace_view)};
76+
if (!backtrace_code_addresses.empty()) {
77+
return backtrace_code_addresses;
78+
}
79+
kphp::log::warning("cannot resolve virtual addresses to debug information");
80+
return {};
3981
}

tests/python/lib/testcase.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ def custom_setup(cls):
207207

208208
if cls.should_use_k2():
209209
kphp_env.update(cls.kphp_env_for_k2_server_component())
210+
kphp_env.update(cls.extra_kphp2cpp_options())
210211

211212
print("\nCompiling kphp")
212213
if not cls.kphp_builder.compile_with_kphp(kphp_env):
@@ -255,6 +256,14 @@ def custom_setup_method(self, method):
255256
def custom_teardown_method(self, method):
256257
self.web_server._engine_logs = []
257258

259+
@classmethod
260+
def extra_kphp2cpp_options(cls):
261+
"""
262+
This method can be overloaded in a test case.
263+
The result should be a dictionary that will be added as environment variables in kphp2cpp
264+
"""
265+
return {}
266+
258267
@classmethod
259268
def extra_class_setup(cls):
260269
"""
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
entry: script
2+
components:
3+
script:
4+
image: KPHP
5+
scope: Request
6+
args: {}
7+
links: {}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
function my_array_find($arr, callable $callback) {
4+
foreach($arr as $element) {
5+
if ($callback($element)) {
6+
return $element;
7+
}
8+
}
9+
return null;
10+
}
11+
12+
function capture_backtrace() {
13+
#ifndef K2
14+
$trace = kphp_backtrace();
15+
$backtrace = [];
16+
foreach($trace as $function) {
17+
$backtrace[] = ["function" => $function];
18+
}
19+
// kphp_backtrace doesn't capture coro trace after coroutine suspending
20+
$backtrace[] = ["function" => "coro_fun3"];
21+
$backtrace[] = ["function" => "coro_fun2"];
22+
$backtrace[] = ["function" => "coro_fun1"];
23+
return $backtrace;
24+
#endif
25+
return debug_backtrace();
26+
}
27+
28+
29+
function check_backtrace($backtrace) {
30+
$ok = true;
31+
$trace = ["sync_fun2", "sync_fun1", "coro_fun3", "coro_fun2", "coro_fun1"];
32+
33+
foreach ($trace as $symbol) {
34+
$ok &= my_array_find($backtrace, function(array $value) use($symbol) {
35+
return stristr($value["function"], $symbol) != false;
36+
}) != null;
37+
if (!$ok) {
38+
return $symbol . " is absent";
39+
}
40+
}
41+
42+
return "ok";
43+
}
44+
45+
46+
function sync_fun2(bool $b) {
47+
while(0);
48+
$trace = capture_backtrace();
49+
return check_backtrace($trace);
50+
}
51+
52+
function sync_fun1(int $x) {
53+
while(0);
54+
return sync_fun2($x > 0);
55+
}
56+
57+
function coro_fun3(?array $x) {
58+
while(0);
59+
sched_yield_sleep(0.01);
60+
$res = sync_fun1($x ? count($x) : -1);
61+
return $res;
62+
}
63+
64+
function coro_fun2(array $x) {
65+
while(0);
66+
return coro_fun3(false ? $x : null);
67+
}
68+
69+
function coro_fun1() {
70+
while(0);
71+
return coro_fun2([1, 2, 3, 4, 5, 6, 7]);
72+
}
73+
74+
function forkable() {
75+
$f = fork(coro_fun1());
76+
return wait($f);
77+
}
78+
79+
function main() {
80+
foreach (json_decode(file_get_contents('php://input')) as $action) {
81+
switch ($action["op"]) {
82+
case "check_main_trace":
83+
$res = coro_fun1();
84+
echo $res;
85+
break;
86+
case "check_fork_trace":
87+
$f = fork(coro_fun1());
88+
$res = wait($f);
89+
echo $res;
90+
break;
91+
case "check_fork_switch_trace":
92+
$f_1 = fork(coro_fun1());
93+
$f_2 = fork(coro_fun3([]));
94+
95+
$res_1 = wait($f_1);
96+
$_ = wait($f_2);
97+
98+
echo $res_1;
99+
break;
100+
case "check_nested_fork_trace":
101+
$f = fork(forkable());
102+
$res = wait($f);
103+
echo $res;
104+
break;
105+
default:
106+
echo "unknown operation";
107+
return;
108+
}
109+
}
110+
}
111+
112+
main();
113+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import pytest
2+
from python.lib.testcase import WebServerAutoTestCase
3+
4+
5+
class TestBacktrace(WebServerAutoTestCase):
6+
@classmethod
7+
def extra_kphp2cpp_options(cls):
8+
return {"KPHP_EXTRA_CXXFLAGS" : "-O0 -g"}
9+
10+
def test_main_fork_trace(self):
11+
resp = self.web_server.http_post(
12+
json=[
13+
{"op": "check_main_trace"},
14+
])
15+
self.assertEqual(resp.text, "ok")
16+
self.assertEqual(resp.status_code, 200)
17+
18+
def test_fork_trace(self):
19+
resp = self.web_server.http_post(
20+
json=[
21+
{"op": "check_fork_trace"},
22+
])
23+
self.assertEqual(resp.text, "ok")
24+
self.assertEqual(resp.status_code, 200)
25+
26+
def test_fork_switch_trace(self):
27+
resp = self.web_server.http_post(
28+
json=[
29+
{"op": "check_fork_switch_trace"},
30+
])
31+
self.assertEqual(resp.text, "ok")
32+
self.assertEqual(resp.status_code, 200)
33+
34+
def test_nested_fork_trace(self):
35+
resp = self.web_server.http_post(
36+
json=[
37+
{"op": "check_nested_fork_trace"},
38+
])
39+
self.assertEqual(resp.text, "ok")
40+
self.assertEqual(resp.status_code, 200)

0 commit comments

Comments
 (0)