mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-06 15:48:49 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fbf550d3c |
Vendored
+33
@@ -177,6 +177,39 @@
|
|||||||
"tbreak main",
|
"tbreak main",
|
||||||
"continue"
|
"continue"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug: Hello Camera! (attach only)",
|
||||||
|
"type": "gdb",
|
||||||
|
"request": "attach",
|
||||||
|
"target": "localhost:3333",
|
||||||
|
"remote": true,
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"valuesFormatting": "parseText",
|
||||||
|
"registerLimit": "1-32",
|
||||||
|
"frameFilters": false,
|
||||||
|
"showDevDebugOutput": false,
|
||||||
|
"printCalls": false,
|
||||||
|
"stopAtConnect": true,
|
||||||
|
"gdbpath": "gdb-multiarch",
|
||||||
|
"windows": {
|
||||||
|
"gdbpath": "gdb-multiarch.exe"
|
||||||
|
},
|
||||||
|
"osx": {
|
||||||
|
"gdbpath": "gdb"
|
||||||
|
},
|
||||||
|
"executable": "${workspaceRoot}/build/hello_camera.dwarf-injected.elf",
|
||||||
|
"setupCommands": [
|
||||||
|
{ "text": "set mi-async off" },
|
||||||
|
{ "text": "set remotetimeout 0" },
|
||||||
|
{ "text": "set logging file build/gen/hello_camera.gdb.log" },
|
||||||
|
{ "text": "set logging redirect on" }
|
||||||
|
],
|
||||||
|
"autorun": [
|
||||||
|
"source scripts/gdb/gdb_tape_atoms.gdb",
|
||||||
|
"tbreak hot_reload_entry",
|
||||||
|
"continue"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-20
@@ -31,11 +31,10 @@
|
|||||||
* From here various conventions can be further applied.
|
* From here various conventions can be further applied.
|
||||||
* To make things easier to understand it may be better to focus on what this
|
* To make things easier to understand it may be better to focus on what this
|
||||||
* ABI does not have. It does not have have any branching within the tape but
|
* ABI does not have. It does not have have any branching within the tape but
|
||||||
* relative branches within atoms or between atoms.
|
* relative branches between atoms. Branching nearly is always downstream.
|
||||||
* Branching nearly is always downstream. Stack usage is non-existent.
|
* Stack usage is non-existent. Push/Pop, FIFO, or Arena/Bump data structures
|
||||||
* Push/Pop, FIFO, or Arena/Bump data structures are used by atoms explicitly.
|
* are used by atoms explicitly. In it's current form withe C11 macro dsl,
|
||||||
* In it's current form withe C11 macro dsl, the user also has to do manual register
|
* the user also has to do manual register allocation per atom.
|
||||||
* allocation per atom.
|
|
||||||
*
|
*
|
||||||
* One of the remarkable things about utilizing this abi is its essentially
|
* One of the remarkable things about utilizing this abi is its essentially
|
||||||
* interopable with CPUs, GPUs, FPGA, or, basically anything
|
* interopable with CPUs, GPUs, FPGA, or, basically anything
|
||||||
@@ -44,38 +43,37 @@
|
|||||||
* in order to execute digital logic effectively on current era tech.
|
* in order to execute digital logic effectively on current era tech.
|
||||||
* On the PS1 we don't have access to a few features like multi-threading,
|
* On the PS1 we don't have access to a few features like multi-threading,
|
||||||
* speculative execution, or L3 cache; but, we can set the foundation for legoing
|
* speculative execution, or L3 cache; but, we can set the foundation for legoing
|
||||||
* whats required for eventually expanding this ABI's paradigm and core atoms
|
* whats required baseline wise for eventually expanding the harness and core atoms
|
||||||
* to take those newer hardware features into account. For example, you can easily
|
* to take those newer hardware features into account. For example, you can easily
|
||||||
* expand this to support wave-based execution model on a PS2 or PS3.
|
* expand this to support wave-based execution model on a PS2 or PS3.
|
||||||
* Not having a stack or automatic register allocation means the user can't ignore
|
* Not having a stack or automatic register allocation means the user can't ignore
|
||||||
* excessive argument shuffle across workload or waves and thier phases.
|
* excessive argument shuffle across workload or waves and thier phases.
|
||||||
* Crossing ABI boundaries to other runtimes that do has obviouss penalties.
|
* Crossing ABI boundaries to other runtimes that do has an obviouss penalties.
|
||||||
*
|
*
|
||||||
* Learning data-oreinted code becomes a natural progression. Your not fighting
|
* Learning data-oreinted code becomes a natural progression. Your not fighting
|
||||||
* a stack-based procedural paradigm that wants to argument shuffle on by lack of
|
* a stack-based procedural paradigm that wants to argument shuffle on the stack
|
||||||
* constraints on how the user may "call" a procedure. The user doesn't
|
* by lack of constraints on how the user may "call" a procedure. The user doesn't
|
||||||
* have to hammer down "rules" or patterns to know how to massage the compiler
|
* have to hammer down "rules" or patterns to know how to massage the compiler
|
||||||
* to dissolve those call frames to get the asesmbly into its desired form.
|
* to get the asesmbly into its natural form. The form is obvious, and once
|
||||||
* The form is obvious, and once the user gets to author these compoonents
|
* the user gets to author their compoonents it becomes a game of tetris.
|
||||||
* it becomes a game of tetris.
|
|
||||||
*
|
*
|
||||||
* Another feature is this ABI is very compatible with bootstrapping and developing
|
* Another feature is this ABI is very compatible with bootstrapping and developing
|
||||||
* simple toolchains built off of bit-packed annotated command streams the user can
|
* simple toolchains built off of bit-packed annotated command streams the user can
|
||||||
* directly author, maintatain, and immediately execute. That being like a color forth.
|
* directly author, maintatain, and immediately execute. That being a color forth.
|
||||||
* This can make the tetris less of a chore with some helpful policy generation for
|
* This can make the tetris less of a chore with some helpful policy generation for
|
||||||
* allocation of registers, helping to choose resuable components, designing DSL on
|
* allocation of registers, helping to choose resuable components, designing DSL on
|
||||||
* the fly, etc.
|
* the fly, etc.
|
||||||
* -----------------------------------------------------------------------------
|
* -----------------------------------------------------------------------------
|
||||||
* TODO(Ed): We need pretty ascii diagrams and proper guides, articles, etc.
|
* TODO(Ed): We ned pretty ascii diagrams and proper guides, articles, etc.
|
||||||
* -----------------------------------------------------------------------------
|
* -----------------------------------------------------------------------------
|
||||||
* For now this ideation is just started functioning. I'm abusing C11 & a lua metaprogram
|
* For now this thing is just functioning and I'm abusing C11 + a lua metaprogram
|
||||||
* to help establish a hybrid toolchain to ideate on a traditional text-based
|
* to help establish a hybrid toolchain to ideate on a traditional text-based
|
||||||
* authoring UX for this paradigm.
|
* authoring UX for this paradigm.
|
||||||
* If pcsx-redux provides viable hot-reload and persistent data storage beyond
|
* If pcsx-redux gets me viable hot-reload and persistent data storage beyond
|
||||||
* save-states (just copying ram to filesystem), I can author a color forth to
|
* save-states (just copying ram to filesystem). I can author a color forth to
|
||||||
* mess around with. With either an editor in-emulator or on the actual machine itself.
|
* mess around with, with an editor in-emulator or on the actual machine itself.
|
||||||
* Assembly is tedius, but I think this codebase most likely has a pretty ergonomic
|
* Assembly is tedius, but I think this codebase most likely has some of the most,
|
||||||
* flavor worst case...
|
* ergonomic you can come across..
|
||||||
* */
|
* */
|
||||||
/* Register Allocation Info */
|
/* Register Allocation Info */
|
||||||
enum {
|
enum {
|
||||||
|
|||||||
@@ -290,6 +290,19 @@ void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_
|
|||||||
}
|
}
|
||||||
|
|
||||||
GCC_OPTIMIZATION_DISABLE
|
GCC_OPTIMIZATION_DISABLE
|
||||||
|
void hot_reload_entry(void)
|
||||||
|
{
|
||||||
|
smem.primitives.used = 0;
|
||||||
|
while (1) {
|
||||||
|
gknown S4* active_buf_id = & smem.active_buf_id;
|
||||||
|
gknown U4* ordering_buf = r_(smem.ordering_tbl)[active_buf_id[0]];
|
||||||
|
gknown PrimitiveArena* pa = & smem.primitives;
|
||||||
|
update(pa, ordering_buf);
|
||||||
|
render();
|
||||||
|
gp_display_frame(& smem.screen_buf, active_buf_id, ordering_buf, pa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
smem = (SMemory){0};
|
smem = (SMemory){0};
|
||||||
@@ -323,14 +336,7 @@ int main(void)
|
|||||||
tb_emit(& tb, gp_screen_init);
|
tb_emit(& tb, gp_screen_init);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (1) {
|
hot_reload_entry();
|
||||||
gknown S4* active_buf_id = & smem.active_buf_id;
|
|
||||||
gknown U4* ordering_buf = r_(smem.ordering_tbl)[active_buf_id[0]];
|
|
||||||
gknown PrimitiveArena* pa = & smem.primitives;
|
|
||||||
update(pa, ordering_buf);
|
|
||||||
render();
|
|
||||||
gp_display_frame(& smem.screen_buf, active_buf_id, ordering_buf, pa);
|
|
||||||
};
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
GCC_OPTIMIZATION_ENABLE
|
GCC_OPTIMIZATION_ENABLE
|
||||||
|
|||||||
+271
-51
@@ -1,3 +1,13 @@
|
|||||||
|
# --- Parameter Surface (Task 8) -----------------------------------------
|
||||||
|
# -Reload : After a successful build, invoke reload.ps1 as a child pwsh and propagate its exit code.
|
||||||
|
# -HelperZipOnly : Skip the build entirely; regenerate the helper zip and exit. Honors -HelperZipOutput for out-of-tree paths.
|
||||||
|
# -HelperZipOutput: When -HelperZipOnly is set, writes the archive to this path instead of the scripts/pcsx_debug_helper.zip.
|
||||||
|
param(
|
||||||
|
[switch]$Reload,
|
||||||
|
[switch]$HelperZipOnly,
|
||||||
|
[string]$HelperZipOutput = ''
|
||||||
|
)
|
||||||
|
|
||||||
$path_root = split-path -Path $PSScriptRoot -Parent
|
$path_root = split-path -Path $PSScriptRoot -Parent
|
||||||
$path_build = join-path $path_root 'build'
|
$path_build = join-path $path_root 'build'
|
||||||
$path_code = join-path $path_root 'code'
|
$path_code = join-path $path_root 'code'
|
||||||
@@ -8,6 +18,98 @@ if ((test-path $path_build) -eq $false) {
|
|||||||
new-item -itemtype directory -path $path_build
|
new-item -itemtype directory -path $path_build
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- HelperZipOnly short-circuit ----------------------------------------
|
||||||
|
# Must run before any compile/link work.
|
||||||
|
# Inlines the same logic as Make-HelperZip below to avoid an extra pwsh process spawn (~200 ms).
|
||||||
|
#The helper zip is small and the BCL call is in-process; cold ~14 ms, warm ~10 ms (assembly load + tiny zip write).
|
||||||
|
if ($HelperZipOnly) {
|
||||||
|
$zipDest = if ([string]::IsNullOrEmpty($HelperZipOutput)) {
|
||||||
|
join-path $path_scripts 'pcsx_debug_helper.zip'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$HelperZipOutput
|
||||||
|
}
|
||||||
|
$HelperDir = join-path $path_scripts 'pcsx_debug_helper'
|
||||||
|
$elf32Src = join-path $path_scripts 'elf32.lua'
|
||||||
|
$elf32Dest = join-path $HelperDir 'elf32.lua'
|
||||||
|
if (-not (test-path -LiteralPath $HelperDir)) {
|
||||||
|
write-error "helper dir not found: $HelperDir"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if (-not (test-path -LiteralPath $elf32Src)) {
|
||||||
|
write-error "elf32.lua not found at $elf32Src"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
write-host "[build] HelperZipOnly mode -> $zipDest"
|
||||||
|
|
||||||
|
# --- Timestamp gate (Fix 1) -------------------------------------------
|
||||||
|
# PCSX-Redux holds pcsx_debug_helper.zip open via -archive at startup.
|
||||||
|
# The zip is consumed once at startup; the reload endpoint reads it
|
||||||
|
# from package.loaded on subsequent calls. Writing it on every build
|
||||||
|
# is dead work that fights the file lock. Skip the rewrite when the
|
||||||
|
# three sources (autoexec.lua, reload.lua, elf32.lua) are all older
|
||||||
|
# than the existing zip.
|
||||||
|
$sources = @(
|
||||||
|
(join-path $HelperDir 'autoexec.lua'),
|
||||||
|
(join-path $HelperDir 'reload.lua'),
|
||||||
|
$elf32Src
|
||||||
|
)
|
||||||
|
$zipMtime = $null
|
||||||
|
if (test-path -LiteralPath $zipDest) {
|
||||||
|
$zipMtime = (Get-Item -LiteralPath $zipDest).LastWriteTime
|
||||||
|
}
|
||||||
|
$needsRewrite = $false
|
||||||
|
if ($null -eq $zipMtime) {
|
||||||
|
$needsRewrite = $true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foreach ($s in $sources) {
|
||||||
|
if (-not (test-path -LiteralPath $s)) { continue }
|
||||||
|
if ((Get-Item -LiteralPath $s).LastWriteTime -gt $zipMtime) {
|
||||||
|
$needsRewrite = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $needsRewrite) {
|
||||||
|
$sz = (Get-Item -LiteralPath $zipDest).Length
|
||||||
|
Write-Host "[build] helper zip up to date: $zipDest ($sz bytes); skipping"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -LiteralPath $elf32Src -Destination $elf32Dest -Force
|
||||||
|
try {
|
||||||
|
# Force the inode release so CreateFromDirectory can write fresh.
|
||||||
|
# ZipFile.CreateFromDirectory throws if the destination exists.
|
||||||
|
# If PCSX-Redux holds the file open, Remove-Item raises — fall
|
||||||
|
# back to writing pcsx_debug_helper.zip.new alongside. The next
|
||||||
|
# PCSX-Redux restart will read the canonical path; the .new file
|
||||||
|
# is a hint for the optional launch-script patch in fix 3.
|
||||||
|
if (test-path -LiteralPath $zipDest) {
|
||||||
|
try {
|
||||||
|
# -ErrorAction Stop is required so the catch below fires.
|
||||||
|
# Remove-Item raises a non-terminating error by default
|
||||||
|
# (ErrorActionPreference=Continue), which bypasses catch.
|
||||||
|
Remove-Item -LiteralPath $zipDest -Force -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$zipDest = [System.IO.Path]::ChangeExtension($zipDest, '.zip.new')
|
||||||
|
Write-Warning "[build] canonical helper zip is locked; writing to $zipDest instead"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
|
[System.IO.Compression.ZipFile]::CreateFromDirectory(
|
||||||
|
$HelperDir, $zipDest,
|
||||||
|
[System.IO.Compression.CompressionLevel]::Optimal, $false) | Out-Null
|
||||||
|
$sz = (Get-Item -LiteralPath $zipDest).Length
|
||||||
|
Write-Host "[build] wrote $sz bytes to $zipDest"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (test-path -LiteralPath $elf32Dest) { Remove-Item -LiteralPath $elf32Dest -Force }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
# --- Toolchain Definition ---
|
# --- Toolchain Definition ---
|
||||||
# Assumes 'mipsel-none-elf' toolchain is in your system's PATH.
|
# Assumes 'mipsel-none-elf' toolchain is in your system's PATH.
|
||||||
$Prefix = "mipsel-none-elf"
|
$Prefix = "mipsel-none-elf"
|
||||||
@@ -180,9 +282,12 @@ function link-modules { param([string[]]$link_modules, [string] $elf, [string[]
|
|||||||
$link_args += ($f_link_pass_through_prefix + $f_link_mapfile + $map)
|
$link_args += ($f_link_pass_through_prefix + $f_link_mapfile + $map)
|
||||||
|
|
||||||
$link_args += ($f_link_pass_through_prefix + $f_link_start_group)
|
$link_args += ($f_link_pass_through_prefix + $f_link_start_group)
|
||||||
# 16 removed entries (c2, card, cd, comb, ds, gs, gun, hmd, math, mcrd, mcx, press, sio, snd, spu, tap)
|
# raw_sio_pad_poll_20260802 — Task 5.1c surgical library-list trim.
|
||||||
# had LOAD lines in the map but ZERO .o files pulled in — they were unused.
|
# The 16 removed entries (c2, card, cd, comb, ds, gs, gun, hmd, math,
|
||||||
# 5 kept libraries (api, c, etc, gpu, gte) are required by the C-side calls in hello_joypad.c (reset_graph, draw_sync, vsync, etc.).
|
# mcrd, mcx, press, sio, snd, spu, tap) had LOAD lines in the map but
|
||||||
|
# ZERO .o files pulled in — they were unused. The 5 kept libraries
|
||||||
|
# (api, c, etc, gpu, gte) are required by the C-side calls in
|
||||||
|
# hello_joypad.c (reset_graph, draw_sync, vsync, etc.).
|
||||||
$libraries = @(
|
$libraries = @(
|
||||||
"api",
|
"api",
|
||||||
"c",
|
"c",
|
||||||
@@ -211,32 +316,68 @@ function link-modules { param([string[]]$link_modules, [string] $elf, [string[]
|
|||||||
function make-binary { param([string]$elf, [string]$exe)
|
function make-binary { param([string]$elf, [string]$exe)
|
||||||
Write-Host "--- Creating Binary ---" -ForegroundColor Cyan
|
Write-Host "--- Creating Binary ---" -ForegroundColor Cyan
|
||||||
write-host "Converting $elf to PS-EXE -> '$exe'"
|
write-host "Converting $elf to PS-EXE -> '$exe'"
|
||||||
$objcopy_args = ($f_objcopy_format + "binary"), $elf, $exe
|
$objcopy_args = ($f_objcopy_format + "binary"), $elf, $exe
|
||||||
& $Objcopy $objcopy_args
|
& $Objcopy $objcopy_args
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Error "Objcopy failed. Aborting."; exit 1 }
|
if ($LASTEXITCODE -ne 0) { Write-Error "Objcopy failed. Aborting."; exit 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
function ps1-meta { param(
|
function ps1-meta { param(
|
||||||
[string]$unity_root,
|
[string] $unity_root,
|
||||||
[string[]]$sources,
|
[string[]]$sources,
|
||||||
[Parameter(Mandatory=$true)][string]$metadata,
|
[Parameter(Mandatory=$true)][string]$metadata,
|
||||||
[string]$out_root = (join-path $path_build 'gen'),
|
[string] $out_root = (join-path $path_build 'gen'),
|
||||||
[string[]]$passes = @('--pre-link'),
|
[string[]]$passes = @('--pre-link'),
|
||||||
[string[]]$extra_args = @()
|
[string[]]$extra_args = @()
|
||||||
)
|
)
|
||||||
# `--unity-root` and `--source` are mutually exclusive. Exactly one of `$unity_root` / `$sources` must be supplied; the other must be absent.
|
# `--unity-root` and `--source` are
|
||||||
|
# mutually exclusive. Exactly one of `$unity_root` / `$sources` must
|
||||||
|
# be supplied; the other must be absent.
|
||||||
if ($null -ne $unity_root -and $unity_root -ne '')
|
if ($null -ne $unity_root -and $unity_root -ne '')
|
||||||
{
|
{
|
||||||
if ($null -ne $sources -and $sources.Count -gt 0) {
|
if ($null -ne $sources -and $sources.Count -gt 0) {
|
||||||
write-error 'ps1-meta: -unity_root and -sources are mutually exclusive'
|
write-error 'ps1-meta: -unity_root and -sources are mutually exclusive'
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif ($null -eq $sources -or $sources.Count -eq 0) {
|
elseif ($null -eq $sources -or $sources.Count -eq 0) {
|
||||||
write-error 'ps1-meta: either -unity_root <file> or -sources <file...> is required'
|
write-error 'ps1-meta: either -unity_root <file> or -sources <file...> is required'
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Defensive attribute clear on tracked gen files ------------------------
|
||||||
|
# Git tracks code/<dir>/gen/*.h files and Windows keeps the Archive bit set
|
||||||
|
# on them. Combined with transient editor locks or co-running processes,
|
||||||
|
# this can make io.open(path, "wb") fail with Access Denied / Sharing
|
||||||
|
# Violation even though Get-ChildItem shows IsReadOnly = False. Clearing
|
||||||
|
# the Read-only + Archive bits locally is safe; git re-asserts them on
|
||||||
|
# the next operation but the metaprogram write always wins.
|
||||||
|
#
|
||||||
|
# Derived from the caller's parameters: $metadata lives in $path_duffle
|
||||||
|
# (so its parent is the duffle dir), and $unity_root / $sources[0] lives
|
||||||
|
# in $path_module (so its parent is the module dir).
|
||||||
|
$pathToDuffle = split-path -Path $metadata -Parent
|
||||||
|
$pathToModule = $null
|
||||||
|
if ($null -ne $unity_root -and $unity_root -ne '') {
|
||||||
|
$pathToModule = split-path -Path $unity_root -Parent
|
||||||
|
}
|
||||||
|
elseif ($null -ne $sources -and $sources.Count -gt 0) {
|
||||||
|
$pathToModule = split-path -Path $sources[0] -Parent
|
||||||
|
}
|
||||||
|
$genFiles = @(
|
||||||
|
join-path $pathToDuffle 'gen\macs.h'
|
||||||
|
join-path $pathToDuffle 'gen\offsets.h'
|
||||||
|
)
|
||||||
|
if ($null -ne $pathToModule) {
|
||||||
|
$genFiles += join-path $pathToModule 'gen\macs.h'
|
||||||
|
$genFiles += join-path $pathToModule 'gen\offsets.h'
|
||||||
|
}
|
||||||
|
foreach ($f in $genFiles) {
|
||||||
|
if (test-path -LiteralPath $f) {
|
||||||
|
attrib -R $f 2>&1 | Out-Null
|
||||||
|
attrib -A $f 2>&1 | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$script = join-path $path_scripts 'ps1_meta.lua'
|
$script = join-path $path_scripts 'ps1_meta.lua'
|
||||||
$input_summary = if ($null -ne $unity_root -and $unity_root -ne '') {
|
$input_summary = if ($null -ne $unity_root -and $unity_root -ne '') {
|
||||||
"unity=$unity_root"
|
"unity=$unity_root"
|
||||||
@@ -265,14 +406,14 @@ function inject-dwarf { param(
|
|||||||
[string]$path_gen
|
[string]$path_gen
|
||||||
)
|
)
|
||||||
$base_name = [System.IO.Path]::GetFileNameWithoutExtension($elf)
|
$base_name = [System.IO.Path]::GetFileNameWithoutExtension($elf)
|
||||||
$path_dwarf_line_bin = join-path $path_gen "$base_name.dwarf_line.bin"
|
$path_dwarf_line_bin = join-path $path_gen "$base_name.dwarf_line.bin"
|
||||||
$path_dwarf_aranges_bin = join-path $path_gen "$base_name.dwarf_aranges.bin"
|
$path_dwarf_aranges_bin = join-path $path_gen "$base_name.dwarf_aranges.bin"
|
||||||
$path_dwarf_rnglists_bin = join-path $path_gen "$base_name.dwarf_rnglists.bin"
|
$path_dwarf_rnglists_bin = join-path $path_gen "$base_name.dwarf_rnglists.bin"
|
||||||
$path_dwarf_info_bin = join-path $path_gen "$base_name.dwarf_info.bin"
|
$path_dwarf_info_bin = join-path $path_gen "$base_name.dwarf_info.bin"
|
||||||
$path_dwarf_abbrev_bin = join-path $path_gen "$base_name.dwarf_abbrev.bin"
|
$path_dwarf_abbrev_bin = join-path $path_gen "$base_name.dwarf_abbrev.bin"
|
||||||
$path_dwarf_str_bin = join-path $path_gen "$base_name.dwarf_str.bin"
|
$path_dwarf_str_bin = join-path $path_gen "$base_name.dwarf_str.bin"
|
||||||
$path_dwarf_loc_bin = join-path $path_gen "$base_name.dwarf_loc.bin"
|
$path_dwarf_loc_bin = join-path $path_gen "$base_name.dwarf_loc.bin"
|
||||||
$path_dwarf_loclists_bin = join-path $path_gen "$base_name.dwarf_loclists.bin"
|
$path_dwarf_loclists_bin = join-path $path_gen "$base_name.dwarf_loclists.bin"
|
||||||
$path_inject_elf = join-path $path_build "$base_name.dwarf-injected.elf"
|
$path_inject_elf = join-path $path_build "$base_name.dwarf-injected.elf"
|
||||||
|
|
||||||
if (-not (Test-Path $path_dwarf_line_bin)) { return }
|
if (-not (Test-Path $path_dwarf_line_bin)) { return }
|
||||||
@@ -517,7 +658,7 @@ function build-hello_camera {
|
|||||||
$path_build_gen = join-path $path_build 'gen'
|
$path_build_gen = join-path $path_build 'gen'
|
||||||
|
|
||||||
$src_c = join-path $path_module 'hello_camera.c'
|
$src_c = join-path $path_module 'hello_camera.c'
|
||||||
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--pre-link')
|
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen
|
||||||
|
|
||||||
$assemble_args = @()
|
$assemble_args = @()
|
||||||
$assemble_args += $f_debug
|
$assemble_args += $f_debug
|
||||||
@@ -552,50 +693,129 @@ function build-hello_camera {
|
|||||||
link-modules $link_modules $elf $link_args
|
link-modules $link_modules $elf $link_args
|
||||||
make-binary $elf $exe
|
make-binary $elf $exe
|
||||||
|
|
||||||
|
# Post-link: gdb-runtime + dwarf-injection in a single Lua invocation (one luajit cold start).
|
||||||
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf)
|
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf)
|
||||||
|
|
||||||
inject-dwarf $elf $path_build_gen
|
inject-dwarf $elf $path_build_gen
|
||||||
}
|
}
|
||||||
build-hello_camera
|
build-hello_camera
|
||||||
|
|
||||||
# NO idea if this works yet...
|
# ── Helper-zip + reload helpers (Task 8) ──
|
||||||
function Send-ToEmulator { param( [string]$exePath )
|
# Defined right after the final build-hello_camera function so they're in scope for the post-build calls below.
|
||||||
$uri = "http://localhost:8080/api/v1/load-exec"
|
# The Make-HelperZip function is also reused by the -HelperZipOnly short-circuit at the top of this script.
|
||||||
|
# Both call the in-process BCL CreateFromDirectory rather than spawning a child pwsh to avoid the ~200 ms process-spawn overhead.
|
||||||
|
function Make-HelperZip {
|
||||||
|
param([string]$OutputPath = '')
|
||||||
|
|
||||||
# Absolute path is safest for the emulator web server
|
$dest = if ([string]::IsNullOrEmpty($OutputPath)) {
|
||||||
$absolutePath = [System.IO.Path]::GetFullPath($exePath)
|
join-path $path_scripts 'pcsx_debug_helper.zip'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$OutputPath
|
||||||
|
}
|
||||||
|
|
||||||
# Create JSON payload pointing to your compiled .ps-exe
|
$HelperDir = join-path $path_scripts 'pcsx_debug_helper'
|
||||||
$body = @{ filename = $absolutePath } | ConvertTo-Json
|
$elf32Src = join-path $path_scripts 'elf32.lua'
|
||||||
|
$elf32Dest = join-path $HelperDir 'elf32.lua'
|
||||||
|
if (-not (test-path -LiteralPath $HelperDir)) {
|
||||||
|
write-warning "[build] helper dir not found: $HelperDir; skipping helper zip"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (-not (test-path -LiteralPath $elf32Src)) {
|
||||||
|
write-warning "[build] elf32.lua not found at $elf32Src; skipping helper zip"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "Pushing hot-reload to PCSX-Redux..." -ForegroundColor Magenta
|
# --- Timestamp gate (Fix 1) -------------------------------------------
|
||||||
try {
|
# PCSX-Redux holds pcsx_debug_helper.zip open via -archive at startup.
|
||||||
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $body -ContentType "application/json"
|
# The zip is consumed once at startup; the reload endpoint reads it
|
||||||
Write-Host "Hot-reload successful!" -ForegroundColor Green
|
# from package.loaded on subsequent calls. Writing it on every build
|
||||||
} catch {
|
# is dead work that fights the file lock. Skip the rewrite when the
|
||||||
Write-Warning "Could not connect to PCSX-Redux web server. Ensure the emulator is running and Web Server is enabled."
|
# three sources (autoexec.lua, reload.lua, elf32.lua) are all older
|
||||||
}
|
# than the existing zip.
|
||||||
|
$sources = @(
|
||||||
|
(join-path $HelperDir 'autoexec.lua'),
|
||||||
|
(join-path $HelperDir 'reload.lua'),
|
||||||
|
$elf32Src
|
||||||
|
)
|
||||||
|
$zipMtime = $null
|
||||||
|
if (test-path -LiteralPath $dest) {
|
||||||
|
$zipMtime = (Get-Item -LiteralPath $dest).LastWriteTime
|
||||||
|
}
|
||||||
|
$needsRewrite = $false
|
||||||
|
if ($null -eq $zipMtime) {
|
||||||
|
$needsRewrite = $true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foreach ($s in $sources) {
|
||||||
|
if (-not (test-path -LiteralPath $s)) { continue }
|
||||||
|
if ((Get-Item -LiteralPath $s).LastWriteTime -gt $zipMtime) {
|
||||||
|
$needsRewrite = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $needsRewrite) {
|
||||||
|
$sz = (Get-Item -LiteralPath $dest).Length
|
||||||
|
Write-Host "[build] helper zip up to date: $dest ($sz bytes); skipping"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
write-host "[build] regenerating helper zip -> $dest"
|
||||||
|
Copy-Item -LiteralPath $elf32Src -Destination $elf32Dest -Force
|
||||||
|
try {
|
||||||
|
# Force the inode release so CreateFromDirectory can write fresh.
|
||||||
|
# ZipFile.CreateFromDirectory throws if the destination exists.
|
||||||
|
# If PCSX-Redux holds the file open, Remove-Item raises — fall
|
||||||
|
# back to writing pcsx_debug_helper.zip.new alongside. The next
|
||||||
|
# PCSX-Redux restart will read the canonical path; the .new file
|
||||||
|
# is a hint for the optional launch-script patch in fix 3.
|
||||||
|
if (test-path -LiteralPath $dest) {
|
||||||
|
try {
|
||||||
|
# -ErrorAction Stop is required so the catch below fires.
|
||||||
|
# Remove-Item raises a non-terminating error by default
|
||||||
|
# (ErrorActionPreference=Continue), which bypasses catch.
|
||||||
|
Remove-Item -LiteralPath $dest -Force -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$dest = [System.IO.Path]::ChangeExtension($dest, '.zip.new')
|
||||||
|
Write-Warning "[build] canonical helper zip is locked; writing to $dest instead"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
|
[System.IO.Compression.ZipFile]::CreateFromDirectory(
|
||||||
|
$HelperDir, $dest,
|
||||||
|
[System.IO.Compression.CompressionLevel]::Optimal, $false) | Out-Null
|
||||||
|
$sz = (Get-Item -LiteralPath $dest).Length
|
||||||
|
Write-Host "[build] wrote $sz bytes to $dest"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (test-path -LiteralPath $elf32Dest) { Remove-Item -LiteralPath $elf32Dest -Force }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# # Automatically hot-reloads it into the running emulator
|
# Invokes reload.ps1 as a child pwsh instead of POSTing to the nonexistent /api/v1/load-exec endpoint.
|
||||||
# Send-ToEmulator (join-path $path_build 'hello_gte.ps-exe')
|
# Exit code is propagated so the build fails loud if the reload fails.
|
||||||
|
function Send-ToEmulator {
|
||||||
|
param([string]$ElfPath = (join-path $path_build 'hello_camera.elf'))
|
||||||
|
|
||||||
# --- Hot Reload via PCSX-Redux Web Server ---
|
$reloadScript = join-path $path_scripts 'reload.ps1'
|
||||||
# $exe_path = join-path $path_build 'hello_gte.ps-exe'
|
if (-not (test-path -LiteralPath $reloadScript)) {
|
||||||
# $absolute_path = [System.IO.Path]::GetFullPath($exe_path)
|
write-error "[build] reload.ps1 not found at $reloadScript"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# PCSX-Redux expects the file location in the URL query string?
|
write-host "[build] hot-reloading $ElfPath via reload.ps1" -ForegroundColor Magenta
|
||||||
# We URL-encode the path to ensure backslashes and spaces don't break the HTTP request?
|
& pwsh -NoProfile -File $reloadScript -Mode elf -Target hello_camera -ElfPath $ElfPath
|
||||||
# $encoded_path = [uri]::EscapeDataString($absolute_path)
|
if ($LASTEXITCODE -ne 0) {
|
||||||
# $uri = "http://localhost:8080/api/v1/load-exec?path=$encoded_path"
|
write-error "[build] reload.ps1 failed (exit $LASTEXITCODE)"
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Write-Host "Pushing hot-reload to PCSX-Redux..." -ForegroundColor Magenta
|
# Post-build: Regenerate the helper zip (canonical output) and, if -Reload was passed, kick a hot-reload against the just-built ELF.
|
||||||
# try {
|
# Any future targets compiled by this script should add their own Make-HelperZip call after their build step; today's only target is hello_camera.
|
||||||
# # Send the request with the query string included
|
Make-HelperZip
|
||||||
# Invoke-RestMethod -Uri $uri -Method Post
|
if ($Reload) {
|
||||||
# Write-Host "Hot-reload successful!" -ForegroundColor Green
|
Send-ToEmulator
|
||||||
# } catch {
|
}
|
||||||
# Write-Host "Failed to hot-reload." -ForegroundColor Red
|
|
||||||
# # This will print the *actual* HTTP error instead of our generic warning
|
|
||||||
# Write-Host $_.Exception.Message -ForegroundColor Yellow
|
|
||||||
# }
|
|
||||||
|
|||||||
@@ -1,418 +0,0 @@
|
|||||||
-- elf32.lua — Pure-Lua ELF32 format helpers with no lfs / no lpeg dependency.
|
|
||||||
-- The reload helper's `parse_manifest` (scripts/pcsx_debug_helper/reload.lua)
|
|
||||||
-- and the metaprogram's `read_elf_sections` + `read_nm` (scripts/elf_dwarf.lua)
|
|
||||||
-- both parsed ELF32 headers from wire bytes.
|
|
||||||
--
|
|
||||||
-- This module contains the format constants and the byte-level walker.
|
|
||||||
--- The metaprogram side keeps `read_u32_le` / `read_u16_le` as local forwarders; the helper side calls `E.*` directly.
|
|
||||||
--
|
|
||||||
-- **Adapter contract (explicit pass style):**
|
|
||||||
-- The helper VM's `Support.File` exposes byte-read methods that require `self` (fileffi.lua:225-227),
|
|
||||||
-- so callers wrap once in a 1-line adapter that strips `self`.
|
|
||||||
-- The parsers here operate on the unwrapped form.
|
|
||||||
-- Reads are flat function calls — `E.read_u8(adapter, off)`, `E.read_u32(adapter, off)`, `E.size(adapter)`.
|
|
||||||
-- read_u8(adapter, off) -> integer | nil
|
|
||||||
-- read_u16(adapter, off) -> integer | nil
|
|
||||||
-- read_u32(adapter, off) -> integer | nil
|
|
||||||
-- size(adapter) -> integer
|
|
||||||
--
|
|
||||||
-- **Convention:** every offset in the constants tables is a zero-based wire offset.
|
|
||||||
-- The `+ 1` conversion happens only at the `string.byte` boundary inside the readers.
|
|
||||||
--
|
|
||||||
-- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table"
|
|
||||||
-- spec: System V ABI gABI v1.2 §"Symbol Table" (Elf32_Sym layout)
|
|
||||||
|
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
-- Little-endian readers (bit-weighted accumulator, math.floor only)
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
--- Read a 4-byte little-endian unsigned integer from `adapter` at zero-based wire offset `off`.
|
|
||||||
---
|
|
||||||
--- Bit weights are written as `0x100`, `0x10000`, `0x1000000` (i.e. 2^8, 2^16, 2^24) so the LE byte positions are visually explicit:
|
|
||||||
--- byte 0 contributes its value directly;
|
|
||||||
--- byte 1 is shifted left by 8; byte 2 by 16; byte 3 by 24.
|
|
||||||
---
|
|
||||||
--- math.floor (not LuaJIT's `>>`) keeps the body portable across LuaJIT 2.0/2.1 and plain Lua 5.x. `string.byte` receives `+ 1` at the boundary.
|
|
||||||
---
|
|
||||||
--- **Call form:** explicit-pass. The reader receives `adapter` as the first positional argument and the offset as the second; no `self` is passed.
|
|
||||||
--- Test fixtures declare `function(offset) ... end` and the parsers call them via dot syntax `adapter.read_u8_at(off)`.
|
|
||||||
--- The colon form `adapter:read_u8_at(off)` would prepend the adapter table as `offset` and break the contract.
|
|
||||||
--- @param adapter table
|
|
||||||
--- @param off integer -- zero-based wire offset
|
|
||||||
--- @return integer|nil
|
|
||||||
function M.read_u32(adapter, off)
|
|
||||||
return adapter.read_u8_at(off)
|
|
||||||
+ adapter.read_u8_at(off + 0x01) * 0x00000100
|
|
||||||
+ adapter.read_u8_at(off + 0x02) * 0x00010000
|
|
||||||
+ adapter.read_u8_at(off + 0x03) * 0x01000000
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Read a 2-byte little-endian unsigned integer from `adapter` at zero-based wire offset `off`.
|
|
||||||
--- @param adapter table
|
|
||||||
--- @param off integer -- zero-based wire offset
|
|
||||||
--- @return integer|nil
|
|
||||||
function M.read_u16(adapter, off)
|
|
||||||
return adapter.read_u8_at(off)
|
|
||||||
+ adapter.read_u8_at(off + 0x01) * 0x00000100
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Read a 1-byte unsigned integer from `adapter` at zero-based wire offset `off`.
|
|
||||||
--- @param adapter table
|
|
||||||
--- @param off integer -- zero-based wire offset
|
|
||||||
--- @return integer|nil
|
|
||||||
function M.read_u8(adapter, off)
|
|
||||||
return adapter.read_u8_at(off)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Total adapter byte length.
|
|
||||||
--- @param adapter table
|
|
||||||
--- @return integer
|
|
||||||
function M.size(adapter)
|
|
||||||
return adapter.read_size()
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Forwarders kept for backward compat with scripts/elf_dwarf.lua.
|
|
||||||
--- The metaprogram side keeps `read_u32_le` / `read_u16_le`;
|
|
||||||
--- both layers now use the same byte-level helpers under the hood.
|
|
||||||
function M.read_u32_le(buf, off)
|
|
||||||
local byte_off = off + 1
|
|
||||||
return buf:byte(byte_off)
|
|
||||||
+ buf:byte(byte_off + 0x01) * 0x00000100
|
|
||||||
+ buf:byte(byte_off + 0x02) * 0x00010000
|
|
||||||
+ buf:byte(byte_off + 0x03) * 0x01000000
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Read a 2-byte little-endian unsigned integer from `buf` at zero-based wire offset `off`.
|
|
||||||
--- @param buf string
|
|
||||||
--- @param off integer -- zero-based wire offset
|
|
||||||
--- @return integer
|
|
||||||
function M.read_u16_le(buf, off)
|
|
||||||
local byte_off = off + 1
|
|
||||||
return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
-- Format constants
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
-- ELF format constants (System V ABI gABI v1.2).
|
|
||||||
M.ELFCLASS32 = 1 -- spec: gABI v1.2 §"ELF Header" — EI_CLASS byte
|
|
||||||
M.ELFDATA2LSB = 1 -- spec: gABI v1.2 §"ELF Header" — EI_DATA byte
|
|
||||||
M.EM_MIPS = 8 -- spec: gABI v1.2 §"Machine Information" — MIPS architecture
|
|
||||||
|
|
||||||
-- Section type constants (System V ABI gABI v1.2 §"Section Header Table").
|
|
||||||
M.SHT_SYMTAB = 2 -- spec: gABI v1.2 §"Section Types" — symbol table
|
|
||||||
M.SHT_STRTAB = 3 -- spec: gABI v1.2 §"Section Types" — string table
|
|
||||||
M.SHT_NOBITS = 8 -- spec: gABI v1.2 §"Section Types" — no space in file
|
|
||||||
|
|
||||||
-- Section flag constants (System V ABI gABI v1.2 §"Section Header Table").
|
|
||||||
M.SHF_WRITE = 0x1 -- spec: gABI v1.2 §"Section Attributes" — writable
|
|
||||||
M.SHF_ALLOC = 0x2 -- spec: gABI v1.2 §"Section Attributes" — occupies memory
|
|
||||||
M.SHF_EXECINSTR = 0x4 -- spec: gABI v1.2 §"Section Attributes" — executable
|
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
|
||||||
-- ELF32 header layout (System V ABI gABI v1.2 §"ELF Header" Table 1)
|
|
||||||
-- ---------------------------------------------------------------------------
|
|
||||||
-- All offsets are zero-based wire offsets. The header is 52 bytes total (header_bytes = 0x34 = 52).
|
|
||||||
M.ELF32_HEADER = {
|
|
||||||
magic_offset = 0x00, -- 4 bytes; expected "\127ELF"
|
|
||||||
magic = "\127ELF",
|
|
||||||
class_offset = 0x04, -- 1 byte; 1 = ELF32, 2 = ELF64
|
|
||||||
endian_offset = 0x05, -- 1 byte; 1 = little-endian, 2 = big-endian
|
|
||||||
header_bytes = 0x34, -- ELF32 header is 52 bytes total
|
|
||||||
e_entry_offset = 0x18, -- 4-byte LE; entry-point virtual address
|
|
||||||
e_shoff_offset = 0x20, -- 4-byte LE; section-header table file offset
|
|
||||||
e_shentsize_offset = 0x2E, -- 2-byte LE; section-header entry size in bytes
|
|
||||||
e_shnum_offset = 0x30, -- 2-byte LE; number of section headers
|
|
||||||
e_shstrndx_offset = 0x32, -- 2-byte LE; index of section-name string table
|
|
||||||
}
|
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
|
||||||
-- ELF32 section-header layout (System V ABI gABI v1.2 §"Section Header Table")
|
|
||||||
-- ---------------------------------------------------------------------------
|
|
||||||
-- Each entry is 40 bytes (sh_entsize_bytes = 0x28 = 40);
|
|
||||||
-- zero-based, field offsets relative to the start of the entry.
|
|
||||||
M.ELF32_SECTION = {
|
|
||||||
sh_name_offset = 0x00, -- 4-byte LE; offset into .shstrtab
|
|
||||||
sh_type_offset = 0x04, -- 4-byte LE; section type (SHT_*)
|
|
||||||
sh_flags_offset = 0x08, -- 4-byte LE; section flags (SHF_*)
|
|
||||||
sh_addr_offset = 0x0C, -- 4-byte LE; virtual address at execution
|
|
||||||
sh_offset_offset = 0x10, -- 4-byte LE; section's file offset
|
|
||||||
sh_size_offset = 0x14, -- 4-byte LE; section's size in bytes
|
|
||||||
sh_link_offset = 0x18, -- 4-byte LE; link to a related section
|
|
||||||
sh_entsize_bytes = 0x28, -- spec: gABI v1.2 §"Section Header Table" — 40 bytes per entry
|
|
||||||
}
|
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
|
||||||
-- ELF32 symbol-table entry layout (System V ABI gABI v1.2 §"Symbol Table")
|
|
||||||
-- ---------------------------------------------------------------------------
|
|
||||||
-- Each entry is 16 bytes (sym_entry_bytes = 0x10 = 16);
|
|
||||||
-- zero-based, field offsets relative to the start of the entry.
|
|
||||||
M.ELF32_SYM = {
|
|
||||||
st_name = 0x00, -- 4-byte LE; offset into the linked string table
|
|
||||||
st_value = 0x04, -- 4-byte LE; symbol value (address / absolute)
|
|
||||||
st_size = 0x08, -- 4-byte LE; symbol size in bytes
|
|
||||||
st_info = 0x0C, -- 1 byte; binding (high nibble) + type (low nibble)
|
|
||||||
sym_entry_bytes = 0x10, -- spec: gABI v1.2 §"Symbol Table" — 16 bytes per entry
|
|
||||||
}
|
|
||||||
|
|
||||||
-- DWARF32 initial-length terminator (DWARF4 §7.4) — kept here so the metaprogram's elf_dwarf.lua can drop its own copy of the same constant.
|
|
||||||
M.dw_dwarf32_terminator = 0xFFFFFFFF
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
-- Adapter validation
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
--- Validate that `adapter` exposes the byte-read surface.
|
|
||||||
--- Returns true on success, false + a stable error code on failure.
|
|
||||||
--- The helper side calls this before parse_manifest to reject callers before any byte is read.
|
|
||||||
--- @param adapter any
|
|
||||||
--- @return boolean, string|nil
|
|
||||||
function M.validate_adapter(adapter)
|
|
||||||
if type(adapter) ~= "table" then return false, "bad_file_adapter" end
|
|
||||||
if type(adapter.read_u8_at) ~= "function" then return false, "bad_file_adapter" end
|
|
||||||
if type(adapter.read_u16_at) ~= "function" then return false, "bad_file_adapter" end
|
|
||||||
if type(adapter.read_u32_at) ~= "function" then return false, "bad_file_adapter" end
|
|
||||||
if type(adapter.read_size) ~= "function" then return false, "bad_file_adapter" end
|
|
||||||
return true, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
-- String-table reader
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
--- Extract a NUL-terminated C string from `strtab` at zero-based offset `off`.
|
|
||||||
--- Returns nil if `off` is out of range or the string is not NUL-terminated.
|
|
||||||
--- @param strtab string
|
|
||||||
--- @param off integer
|
|
||||||
--- @return string|nil
|
|
||||||
function M.get_str(strtab, off)
|
|
||||||
if off < 0 or off >= #strtab then return nil end
|
|
||||||
local end_pos = strtab:find("\0", off + 1, true)
|
|
||||||
if not end_pos then return nil end
|
|
||||||
return strtab:sub(off + 1, end_pos - 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
-- Header / section / symbol walkers
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
--- Read the ELF32 header through `adapter` and validate the magic, class, and data encoding.
|
|
||||||
--- Returns a table on success:
|
|
||||||
--- { e_entry, e_shoff, e_shentsize, e_shnum, e_shstrndx, error = nil }
|
|
||||||
--- On failure returns nil + a stable error code:
|
|
||||||
--- bad_magic, unsupported_elf_class, unsupported_elf_data, truncated_header
|
|
||||||
--- The header's machine field is NOT validated here — callers (e.g. the helper's prime path) decide whether to require EM_MIPS before symbol reads.
|
|
||||||
--- @param adapter table
|
|
||||||
--- @return table|nil, string|nil
|
|
||||||
function M.parse_elf32_headers(adapter)
|
|
||||||
local ok, err = M.validate_adapter(adapter)
|
|
||||||
if not ok then return nil, err end
|
|
||||||
|
|
||||||
-- 4-byte magic: 0x7F 'E' 'L' 'F'.
|
|
||||||
-- The byte readers take the adapter explicitly.
|
|
||||||
-- The production `Support.File` adapter is wrapped by the caller to drop its implicit `self` so the parser shape is flat pass-style.
|
|
||||||
local b1 = M.read_u8(adapter, 0)
|
|
||||||
local b2 = M.read_u8(adapter, 1)
|
|
||||||
local b3 = M.read_u8(adapter, 2)
|
|
||||||
local b4 = M.read_u8(adapter, 3)
|
|
||||||
if not (b1 and b2 and b3 and b4)
|
|
||||||
or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then
|
|
||||||
return nil, "bad_magic"
|
|
||||||
end
|
|
||||||
|
|
||||||
local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset)
|
|
||||||
if class ~= M.ELFCLASS32 then
|
|
||||||
return nil, "unsupported_elf_class"
|
|
||||||
end
|
|
||||||
|
|
||||||
local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset)
|
|
||||||
if data ~= M.ELFDATA2LSB then
|
|
||||||
return nil, "unsupported_elf_data"
|
|
||||||
end
|
|
||||||
|
|
||||||
local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset)
|
|
||||||
local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset)
|
|
||||||
local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset)
|
|
||||||
local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset)
|
|
||||||
local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset)
|
|
||||||
if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then
|
|
||||||
return nil, "truncated_header"
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
e_entry = e_entry,
|
|
||||||
e_shoff = e_shoff,
|
|
||||||
e_shentsize = e_shentsize,
|
|
||||||
e_shnum = e_shnum,
|
|
||||||
e_shstrndx = e_shstrndx,
|
|
||||||
error = nil,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Read one section-header entry from `adapter` at `sh_off`.
|
|
||||||
--- Returns a table with the wire fields plus a (yet-unresolved) `name` field.
|
|
||||||
--- @param adapter table
|
|
||||||
--- @param sh_off integer
|
|
||||||
--- @return table|nil, string|nil -- entry, error
|
|
||||||
local function read_section_entry(adapter, sh_off)
|
|
||||||
local entry = {
|
|
||||||
sh_name = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_name_offset),
|
|
||||||
sh_type = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_type_offset),
|
|
||||||
sh_flags = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_flags_offset),
|
|
||||||
sh_addr = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_addr_offset),
|
|
||||||
sh_offset = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_offset_offset),
|
|
||||||
sh_size = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_size_offset),
|
|
||||||
sh_link = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_link_offset),
|
|
||||||
name = "",
|
|
||||||
}
|
|
||||||
if not (entry.sh_name and entry.sh_type and entry.sh_flags and entry.sh_addr
|
|
||||||
and entry.sh_offset and entry.sh_size and entry.sh_link) then
|
|
||||||
return nil, "truncated_section_headers"
|
|
||||||
end
|
|
||||||
return entry, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Walk every section header in `hdr` and return a 1-based array of entries
|
|
||||||
--- (the section at logical index 0 is at array position 1, etc.).
|
|
||||||
--- Each entry has the wire fields plus a resolved `name` derived from `.shstrtab`.
|
|
||||||
--- Returns nil + a stable error code on failure: truncated_section_headers, missing_shstrtab, truncated_strtab
|
|
||||||
--- @param adapter table
|
|
||||||
--- @param hdr table -- the table returned by parse_elf32_headers
|
|
||||||
--- @return table|nil, string|nil
|
|
||||||
function M.walk_sections(adapter, hdr)
|
|
||||||
if not hdr or hdr.error then return nil, hdr and hdr.error or "truncated_section_headers" end
|
|
||||||
|
|
||||||
local file_size = M.size(adapter)
|
|
||||||
if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then
|
|
||||||
return nil, "truncated_section_headers"
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Read every section header first; we need .shstrtab to resolve names.
|
|
||||||
local sections = {}
|
|
||||||
for i = 0, hdr.e_shnum - 1 do
|
|
||||||
local sh_off = hdr.e_shoff + i * hdr.e_shentsize
|
|
||||||
local entry, err = read_section_entry(adapter, sh_off)
|
|
||||||
if not entry then return nil, err end
|
|
||||||
sections[i + 1] = entry
|
|
||||||
end
|
|
||||||
|
|
||||||
if hdr.e_shstrndx >= hdr.e_shnum then
|
|
||||||
return nil, "missing_shstrtab"
|
|
||||||
end
|
|
||||||
|
|
||||||
local shstrtab = sections[hdr.e_shstrndx + 1]
|
|
||||||
if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then
|
|
||||||
return nil, "missing_shstrtab"
|
|
||||||
end
|
|
||||||
if shstrtab.sh_offset + shstrtab.sh_size > file_size then
|
|
||||||
return nil, "truncated_section_headers"
|
|
||||||
end
|
|
||||||
local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab)
|
|
||||||
if not shstrtab_bytes then return nil, "truncated_section_headers" end
|
|
||||||
|
|
||||||
for _, s in ipairs(sections) do
|
|
||||||
s.name = M.get_str(shstrtab_bytes, s.sh_name) or ""
|
|
||||||
end
|
|
||||||
|
|
||||||
return sections, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Read the bytes of one section. Returns a string, or nil if the adapter returns nil for any byte (out-of-bounds).
|
|
||||||
--- The caller is responsible fors sizing the buffer (the section's sh_offset + sh_size must fit in adapter.size).
|
|
||||||
--- @param adapter table
|
|
||||||
--- @param section table -- one entry from walk_sections
|
|
||||||
--- @return string|nil
|
|
||||||
function M.read_section_bytes(adapter, section)
|
|
||||||
local size = section.sh_size
|
|
||||||
if size == 0 then return "" end
|
|
||||||
local out = {}
|
|
||||||
for i = 0, size - 1 do
|
|
||||||
local b = M.read_u8(adapter, section.sh_offset + i)
|
|
||||||
if b == nil then return nil end
|
|
||||||
out[#out + 1] = string.char(b)
|
|
||||||
end
|
|
||||||
return table.concat(out)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Convenience: walk sections, then look up the named section, then read its bytes.
|
|
||||||
--- Returns nil + a stable error code if the section is absent or out-of-bounds.
|
|
||||||
--- @param adapter table
|
|
||||||
--- @param sections table -- 1-based array from walk_sections
|
|
||||||
--- @param name string
|
|
||||||
--- @return string|nil, string|nil
|
|
||||||
function M.read_named_section(adapter, sections, name)
|
|
||||||
if not sections then return nil, "missing_section" end
|
|
||||||
for _, s in ipairs(sections) do
|
|
||||||
if s.name == name then
|
|
||||||
local bytes = M.read_section_bytes(adapter, s)
|
|
||||||
if not bytes then return nil, "truncated_section_data" end
|
|
||||||
return bytes, nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil, "missing_section"
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Walk every SHT_SYMTAB section in `sections` and accumulate symbols by name.
|
|
||||||
--- Each stored entry is `{ value = st_value, size = st_size, info = st_info, shndx = st_shndx }`.
|
|
||||||
--- Both STB_LOCAL and STB_GLOBAL symbols are included; the live ELF stores `smem` as a local symbol.
|
|
||||||
--- Returns nil + a stable error code on failure: missing_symtab_strtab, truncated_section_headers
|
|
||||||
--- @param adapter table
|
|
||||||
--- @param sections table
|
|
||||||
--- @return table|nil, string|nil
|
|
||||||
function M.collect_symbols(adapter, sections)
|
|
||||||
if not sections then return nil, "missing_sections" end
|
|
||||||
local symbols = {}
|
|
||||||
local file_size = M.size(adapter)
|
|
||||||
for _, s in ipairs(sections) do
|
|
||||||
if s.sh_type == M.SHT_SYMTAB then
|
|
||||||
local strtab = sections[s.sh_link + 1]
|
|
||||||
if not strtab or strtab.sh_type ~= M.SHT_STRTAB then
|
|
||||||
return nil, "missing_symtab_strtab"
|
|
||||||
end
|
|
||||||
if strtab.sh_offset + strtab.sh_size > file_size then
|
|
||||||
return nil, "truncated_section_headers"
|
|
||||||
end
|
|
||||||
local strtab_bytes = M.read_section_bytes(adapter, strtab)
|
|
||||||
if not strtab_bytes then return nil, "truncated_section_headers" end
|
|
||||||
if s.sh_offset + s.sh_size > file_size then
|
|
||||||
return nil, "truncated_section_headers"
|
|
||||||
end
|
|
||||||
local symtab_bytes = M.read_section_bytes(adapter, s)
|
|
||||||
if not symtab_bytes then return nil, "truncated_section_headers" end
|
|
||||||
local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes
|
|
||||||
for j = 0, n - 1 do
|
|
||||||
local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes
|
|
||||||
local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name)
|
|
||||||
if st_name then
|
|
||||||
local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value)
|
|
||||||
local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size)
|
|
||||||
local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info)
|
|
||||||
-- st_shndx is at offset 14 (2 bytes) — derived from the layout
|
|
||||||
-- the metaprogram reads too. Inline the read to keep the
|
|
||||||
-- adapter as the only I/O surface.
|
|
||||||
local b1 = M.read_u8(adapter, e + 14)
|
|
||||||
local b2 = M.read_u8(adapter, e + 15)
|
|
||||||
if not (b1 and b2) then
|
|
||||||
return nil, "truncated_section_headers"
|
|
||||||
end
|
|
||||||
local st_shndx = b1 + b2 * 0x100
|
|
||||||
local name = M.get_str(strtab_bytes, st_name) or ""
|
|
||||||
if name ~= "" then
|
|
||||||
symbols[name] = {
|
|
||||||
value = st_value,
|
|
||||||
size = st_size,
|
|
||||||
info = st_info,
|
|
||||||
shndx = st_shndx,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return symbols, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
+140
-153
@@ -11,11 +11,6 @@
|
|||||||
-- lfs is wired into package.cpath by `duffle_paths.lua` (vendored under `toolchain/lfs/lfs.dll`).
|
-- lfs is wired into package.cpath by `duffle_paths.lua` (vendored under `toolchain/lfs/lfs.dll`).
|
||||||
local lfs = require("lfs")
|
local lfs = require("lfs")
|
||||||
|
|
||||||
-- scripts/elf32.lua contains format-constant tables + the byte-level walker.
|
|
||||||
-- The this file re-exports `read_u32_le` / `read_u16_le` (and the DWARF32 terminator).
|
|
||||||
-- TODO(Ed): Remove re-export.
|
|
||||||
local E = require("elf32")
|
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -107,13 +102,27 @@ M.MIPS_BYTES_PER_WORD = 0x04
|
|||||||
--- **Wire-offset contract:** format offsets, fixed-width reader offsets, LEB/parser cursors, and section-relative values are zero-based wire offsets.
|
--- **Wire-offset contract:** format offsets, fixed-width reader offsets, LEB/parser cursors, and section-relative values are zero-based wire offsets.
|
||||||
--- Only Lua string APIs receive a `+ 1` conversion at their boundary (`byte`, `sub`, and `find`).
|
--- Only Lua string APIs receive a `+ 1` conversion at their boundary (`byte`, `sub`, and `find`).
|
||||||
--- ELF/DWARF field offsets are expressed in hex so they map directly to the zero-based byte positions in the binary file.
|
--- ELF/DWARF field offsets are expressed in hex so they map directly to the zero-based byte positions in the binary file.
|
||||||
---
|
|
||||||
--- The ELF32 header / section / sym layout tables are within scripts/elf32.lua.
|
|
||||||
--- The metaprogram re-exports the DWARF32 initial-length terminator.
|
|
||||||
|
|
||||||
--- spec: DWARF4 spec §7.4 — 32-bit DWARF initial-length terminator
|
--- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table"
|
||||||
M.dw_dwarf32_terminator = E.dw_dwarf32_terminator
|
M.ELF32 = {
|
||||||
-- TODO(Ed): Remove re-export.
|
magic_offset = 0x00, -- 4-byte magic "\127ELF" at file offset 0x00
|
||||||
|
magic = "\127ELF",
|
||||||
|
class_offset = 0x04, -- 1-byte; 1 = ELF32, 2 = ELF64
|
||||||
|
class_elf32 = 1,
|
||||||
|
endian_offset = 0x05, -- 1-byte; 1 = little-endian, 2 = big-endian
|
||||||
|
endian_little = 1,
|
||||||
|
header_bytes = 0x34, -- spec: gABI v1.2 §"ELF Header" — ELF32 header is 52 bytes total
|
||||||
|
e_shoff_offset = 0x20, -- 4-byte LE; section-header table file offset
|
||||||
|
e_shentsize_offset = 0x2E, -- 2-byte LE; section-header entry size in bytes
|
||||||
|
e_shnum_offset = 0x30, -- 2-byte LE; number of section headers
|
||||||
|
e_shstrndx_offset = 0x32, -- 2-byte LE; index of section-name string table
|
||||||
|
sh_size_bytes = 0x28, -- spec: gABI v1.2 §"Section Header Table" — each entry is 40 bytes
|
||||||
|
sh_name_offset = 0x00, -- 4-byte LE; offset into .shstrtab
|
||||||
|
sh_type_offset = 0x04, -- 4-byte LE; section type (SHT_*)
|
||||||
|
sh_offset_offset = 0x10, -- 4-byte LE; section's file offset
|
||||||
|
sh_size_offset = 0x14, -- 4-byte LE; section's size in bytes
|
||||||
|
dw_dwarf32_terminator = 0xFFFFFFFF, -- spec: DWARF4 spec §7.4 — 32-bit DWARF initial-length terminator
|
||||||
|
}
|
||||||
|
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
-- DWARF4 .debug_aranges (per DWARF5 spec §7.4 — Address Range Table)
|
-- DWARF4 .debug_aranges (per DWARF5 spec §7.4 — Address Range Table)
|
||||||
@@ -232,24 +241,27 @@ M.DWARF5_DEBUG_LINE = {
|
|||||||
--- (which has partial `string.unpack` coverage).
|
--- (which has partial `string.unpack` coverage).
|
||||||
--- **Convention:** `off` is a zero-based wire offset; `+ 1` is applied only at the `string.byte` boundary.
|
--- **Convention:** `off` is a zero-based wire offset; `+ 1` is applied only at the `string.byte` boundary.
|
||||||
---
|
---
|
||||||
--- Thin forwarder: the canonical implementation lives in scripts/elf32.lua.
|
--- **Byte weights** are written as `0x100`, `0x10000`, `0x1000000` (i.e. 2^8, 2^16, 2^24) so the LE byte positions are visually explicit:
|
||||||
--- The "second caller lifts" pattern keeps the metaprogram side fluent
|
--- byte 0 contributes its value directly; byte 1 is shifted left by 8 (= 0x100); byte 2 by 16 (= 0x10000); byte 3 by 24 (= 0x1000000).
|
||||||
--- (`M.read_u32_le(buf, off)`) while the body is deduped.
|
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param off integer -- zero-based wire offset
|
--- @param off integer -- zero-based wire offset
|
||||||
--- @return integer
|
--- @return integer
|
||||||
function M.read_u32_le(buf, off)
|
function M.read_u32_le(buf, off)
|
||||||
return E.read_u32_le(buf, off)
|
local byte_off = off + 1
|
||||||
|
return buf:byte(byte_off)
|
||||||
|
+ buf:byte(byte_off + 0x01) * 0x00000100
|
||||||
|
+ buf:byte(byte_off + 0x02) * 0x00010000
|
||||||
|
+ buf:byte(byte_off + 0x03) * 0x01000000
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Read a 2-byte little-endian unsigned integer from `buf` at zero-based wire offset `off`.
|
--- Read a 2-byte little-endian unsigned integer from `buf` at zero-based wire offset `off`.
|
||||||
--- (`off` is zero-based; `+ 1` is applied only at the `string.byte` boundary.)
|
--- (`off` is zero-based; `+ 1` is applied only at the `string.byte` boundary.)
|
||||||
--- Thin forwarder — see `M.read_u32_le` for the rationale.
|
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param off integer -- zero-based wire offset
|
--- @param off integer -- zero-based wire offset
|
||||||
--- @return integer
|
--- @return integer
|
||||||
function M.read_u16_le(buf, off)
|
function M.read_u16_le(buf, off)
|
||||||
return E.read_u16_le(buf, off)
|
local byte_off = off + 1
|
||||||
|
return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches the existing parser.
|
-- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches the existing parser.
|
||||||
@@ -430,20 +442,20 @@ function M.read_ref_sig8(buf, pos)
|
|||||||
return M.read_u32_le(buf, pos), M.read_u32_le(buf, pos + 4), pos + 8
|
return M.read_u32_le(buf, pos), M.read_u32_le(buf, pos + 4), pos + 8
|
||||||
end
|
end
|
||||||
|
|
||||||
--- DWARF5 §7.5.6 (Type Entries).
|
-- DWARF5 §7.5.6 (Type Entries).
|
||||||
--- Walk all units in `info` and return the 0-based offset of the first unit whose `DW_AT_type_signature`
|
-- Walk all units in `info` and return the 0-based offset of the first unit
|
||||||
--- (8-byte value at the end of the unit header) equals `target_sig`.
|
-- whose `DW_AT_type_signature` (8-byte value at the end of the unit header) equals `target_sig`.
|
||||||
--- The signature is interpreted as two 32-bit halves (low/high) per the read_ref_sig8 contract;
|
-- The signature is interpreted as two 32-bit halves (low/high) per the read_ref_sig8 contract;
|
||||||
--- we match both halves (i.e. the 8-byte value as a whole). Returns nil if no matching unit exists.
|
-- we match both halves (i.e. the 8-byte value as a whole). Returns nil if no matching unit exists.
|
||||||
---
|
--
|
||||||
--- Unit header layout (from pos 0):
|
-- Unit header layout (from pos 0):
|
||||||
--- unit_length(4) + version(2) + unit_type(1) + address_size(1) + debug_abbrev_offset(4)
|
-- unit_length(4) + version(2) + unit_type(1) + address_size(1) + debug_abbrev_offset(4)
|
||||||
--- followed by type_unit_specific fields: type_signature(8) + type_offset(4)
|
-- followed by type_unit_specific fields: type_signature(8) + type_offset(4)
|
||||||
--- The type_signature is at byte offset 8 of the body (right after debug_abbrev_offset).
|
-- The type_signature is at byte offset 8 of the body (right after debug_abbrev_offset).
|
||||||
--- @param info string -- the .debug_info section bytes
|
-- @param info string -- the .debug_info section bytes
|
||||||
--- @param target_sig_lo integer -- low 4 bytes (LE) of the desired signature
|
-- @param target_sig_lo integer -- low 4 bytes (LE) of the desired signature
|
||||||
--- @param target_sig_hi integer -- high 4 bytes (LE) of the desired signature
|
-- @param target_sig_hi integer -- high 4 bytes (LE) of the desired signature
|
||||||
--- @return integer|nil, integer|nil -- unit offset, type_offset within the unit
|
-- @return integer|nil, integer|nil -- unit offset, type_offset within the unit
|
||||||
function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
|
function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
|
||||||
local pos = 0
|
local pos = 0
|
||||||
local section_len = #info
|
local section_len = #info
|
||||||
@@ -527,7 +539,7 @@ end
|
|||||||
--- (we walk all `e_shnum` headers regardless of how many names are requested, to find the .shstrtab first).
|
--- (we walk all `e_shnum` headers regardless of how many names are requested, to find the .shstrtab first).
|
||||||
--- For frequent callers, pass the union of all needed sections in one call.
|
--- For frequent callers, pass the union of all needed sections in one call.
|
||||||
-- Can add `.debug_info` + `.debug_loc` + `.debug_str_offsets` to the list without writing a 2nd ELF walker.
|
-- Can add `.debug_info` + `.debug_loc` + `.debug_str_offsets` to the list without writing a 2nd ELF walker.
|
||||||
--- @param elf_path Path
|
--- @param elf_path Path
|
||||||
--- @param section_names string[] -- list of section names to read
|
--- @param section_names string[] -- list of section names to read
|
||||||
--- @return table<string, string>
|
--- @return table<string, string>
|
||||||
function M.read_elf_sections(elf_path, section_names)
|
function M.read_elf_sections(elf_path, section_names)
|
||||||
@@ -552,58 +564,69 @@ function M.read_elf_sections(elf_path, section_names)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
local file_size
|
-- Read the ELF32 header.
|
||||||
do
|
local header = f:read(M.ELF32.header_bytes)
|
||||||
f:seek("end", 0)
|
if not header or #header < M.ELF32.header_bytes then
|
||||||
file_size = f:seek("cur", 0)
|
io.stderr:write("[elf_dwarf.read_elf_sections] ELF too small for ELF32 header\n")
|
||||||
end
|
|
||||||
local adapter = {
|
|
||||||
read_u8_at = function(offset)
|
|
||||||
f:seek("set", offset)
|
|
||||||
local b = f:read(1)
|
|
||||||
if not b then return nil end
|
|
||||||
return b:byte()
|
|
||||||
end,
|
|
||||||
read_u16_at = function(offset)
|
|
||||||
f:seek("set", offset)
|
|
||||||
local b1 = f:read(1)
|
|
||||||
local b2 = f:read(1)
|
|
||||||
if not b1 or not b2 then return nil end
|
|
||||||
return b1:byte() + b2:byte() * 0x100
|
|
||||||
end,
|
|
||||||
read_u32_at = function(offset)
|
|
||||||
f:seek("set", offset)
|
|
||||||
local b1 = f:read(1)
|
|
||||||
local b2 = f:read(1)
|
|
||||||
local b3 = f:read(1)
|
|
||||||
local b4 = f:read(1)
|
|
||||||
if not b1 or not b2 or not b3 or not b4 then return nil end
|
|
||||||
return b1:byte() + b2:byte() * 0x100
|
|
||||||
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000
|
|
||||||
end,
|
|
||||||
read_size = function() return file_size end,
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Delegate the header parse + section walk to E.*.
|
|
||||||
local hdr, hdr_err = E.parse_elf32_headers(adapter)
|
|
||||||
if not hdr then
|
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] header parse failed: %s\n", tostring(hdr_err)))
|
|
||||||
f:close()
|
f:close()
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
local sections, walk_err = E.walk_sections(adapter, hdr)
|
-- Sanity-check magic + class + endianness.
|
||||||
if not sections then
|
if header:sub(M.ELF32.magic_offset + 1, M.ELF32.magic_offset + 0x04) ~= M.ELF32.magic then
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] section walk failed: %s\n", tostring(walk_err)))
|
io.stderr:write("[elf_dwarf.read_elf_sections] not an ELF file\n")
|
||||||
|
f:close()
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
if header:byte(M.ELF32.class_offset + 1) ~= M.ELF32.class_elf32 then
|
||||||
|
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] not ELF32 (class=%d)\n", header:byte(M.ELF32.class_offset + 1)))
|
||||||
|
f:close()
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
if header:byte(M.ELF32.endian_offset + 1) ~= M.ELF32.endian_little then
|
||||||
|
io.stderr:write("[elf_dwarf.read_elf_sections] not little-endian; unsupported\n")
|
||||||
f:close()
|
f:close()
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Resolve the requested sections.
|
-- Parse section-header table location + dimensions from the header.
|
||||||
for _, s in ipairs(sections) do
|
local e_shoff = M.read_u32_le(header, M.ELF32.e_shoff_offset)
|
||||||
if wanted[s.name] then
|
local e_shentsize = M.read_u16_le(header, M.ELF32.e_shentsize_offset)
|
||||||
local bytes = E.read_section_bytes(adapter, s)
|
local e_shnum = M.read_u16_le(header, M.ELF32.e_shnum_offset)
|
||||||
if bytes then result[s.name] = bytes end
|
local e_shstrndx = M.read_u16_le(header, M.ELF32.e_shstrndx_offset)
|
||||||
|
|
||||||
|
-- Read the section-header string table (.shstrtab) so we can resolve section names from their `sh_name` offsets.
|
||||||
|
f:seek("set", e_shoff + e_shstrndx * e_shentsize)
|
||||||
|
local strtab_hdr = f:read(e_shentsize)
|
||||||
|
if not strtab_hdr or #strtab_hdr < e_shentsize then
|
||||||
|
io.stderr:write("[elf_dwarf.read_elf_sections] could not read .shstrtab header\n")
|
||||||
|
f:close()
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
local strtab_offset = M.read_u32_le(strtab_hdr, M.ELF32.sh_offset_offset)
|
||||||
|
local strtab_size = M.read_u32_le(strtab_hdr, M.ELF32.sh_size_offset)
|
||||||
|
f:seek("set", strtab_offset)
|
||||||
|
local strtab = f:read(strtab_size) or ""
|
||||||
|
|
||||||
|
-- Walk all section headers; collect (offset, size) for the wanted names.
|
||||||
|
local function read_section_bytes(sh_offset, sh_size)
|
||||||
|
f:seek("set", sh_offset)
|
||||||
|
return f:read(sh_size) or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
for sh_idx = 0, e_shnum - 1 do
|
||||||
|
f:seek("set", e_shoff + sh_idx * e_shentsize)
|
||||||
|
local sh = f:read(e_shentsize)
|
||||||
|
if not sh or #sh < e_shentsize then break end
|
||||||
|
local sh_name = M.read_u32_le(sh, M.ELF32.sh_name_offset)
|
||||||
|
local sh_offset = M.read_u32_le(sh, M.ELF32.sh_offset_offset)
|
||||||
|
local sh_size = M.read_u32_le(sh, M.ELF32.sh_size_offset)
|
||||||
|
|
||||||
|
-- Extract the name (null-terminated C string in strtab).
|
||||||
|
local name_end = strtab:find("\0", sh_name + 1, true) or (sh_name + 1)
|
||||||
|
local name = strtab:sub(sh_name + 1, name_end - 1)
|
||||||
|
if wanted[name] then
|
||||||
|
result[name] = read_section_bytes(sh_offset, sh_size)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -620,87 +643,48 @@ end
|
|||||||
--- - We filter on STB_GLOBAL (high nibble of st_info = 1) to match `nm`'s default (external symbols only). STB_WEAK excluded.
|
--- - We filter on STB_GLOBAL (high nibble of st_info = 1) to match `nm`'s default (external symbols only). STB_WEAK excluded.
|
||||||
--- - The `code_` prefix is stripped (MipsAtom_ macros emit bare atom names, no `code_` prefix).
|
--- - The `code_` prefix is stripped (MipsAtom_ macros emit bare atom names, no `code_` prefix).
|
||||||
--- - `st_size > 0` filter excludes undefined/imported symbols.
|
--- - `st_size > 0` filter excludes undefined/imported symbols.
|
||||||
---
|
|
||||||
--- @param elf_path Path
|
--- @param elf_path Path
|
||||||
--- @return table<string, {integer, integer}>
|
--- @return table<string, {integer, integer}>
|
||||||
function M.read_nm(elf_path)
|
function M.read_nm(elf_path)
|
||||||
local addrs = {}
|
local addrs = {}
|
||||||
|
|
||||||
-- Existence check first; an empty or missing ELF returns an empty map.
|
-- Read .symtab + .strtab via the existing ELF walker (no subprocess).
|
||||||
if lfs.attributes(elf_path, "mode") ~= "file" then
|
local sections = M.read_elf_sections(elf_path, {".symtab", ".strtab"})
|
||||||
|
local symtab = sections[".symtab"]
|
||||||
|
local strtab = sections[".strtab"]
|
||||||
|
if not symtab or not strtab or #symtab == 0 or #strtab == 0 then
|
||||||
|
-- No symbol table (e.g. stripped ELF). Return empty.
|
||||||
return addrs
|
return addrs
|
||||||
end
|
end
|
||||||
|
|
||||||
local f = io.open(elf_path, "rb")
|
-- Iterate the 16-byte ELF32 symtab entries.
|
||||||
if not f then
|
-- Each entry (zero-based): st_name at 0, st_value at 4, st_size at 8, st_info at 12, st_other at 13, st_shndx at 14.
|
||||||
return addrs
|
local SYM_ENTRY_BYTES = 0x10
|
||||||
end
|
local SYM_ST_NAME = 0x00
|
||||||
|
local SYM_ST_VALUE = 0x04
|
||||||
-- Build the file adapter for E.*.
|
local SYM_ST_SIZE = 0x08
|
||||||
local file_size
|
local SYM_ST_INFO = 0x0C
|
||||||
do
|
local n_syms = #symtab / SYM_ENTRY_BYTES
|
||||||
f:seek("end", 0)
|
for i = 0, n_syms - 1 do
|
||||||
file_size = f:seek("cur", 0)
|
local entry_off = i * SYM_ENTRY_BYTES
|
||||||
end
|
local st_info = symtab:byte(entry_off + SYM_ST_INFO + 1)
|
||||||
local adapter = {
|
-- High nibble = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2).
|
||||||
read_u8_at = function(offset)
|
-- Use math.floor(/16) instead of bit.rshift for LuaJIT 2.1 compat (LuaJIT's `>>` is 5.3+, but math.floor(x/16) works on all versions).
|
||||||
f:seek("set", offset)
|
local binding = math.floor(st_info / 16)
|
||||||
local b = f:read(1)
|
if binding == 0 or binding == 1 then -- STB_LOCAL or STB_GLOBAL
|
||||||
if not b then return nil end
|
local st_size = M.read_u32_le(symtab, entry_off + SYM_ST_SIZE)
|
||||||
return b:byte()
|
if st_size > 0 then
|
||||||
end,
|
local st_name_off = M.read_u32_le(symtab, entry_off + SYM_ST_NAME)
|
||||||
read_u16_at = function(offset)
|
-- Extract the name from .strtab (null-terminated C string).
|
||||||
f:seek("set", offset)
|
local name_end = strtab:find("\0", st_name_off + 1, true) or (st_name_off + 1)
|
||||||
local b1 = f:read(1)
|
local name = strtab:sub(st_name_off + 1, name_end - 1)
|
||||||
local b2 = f:read(1)
|
-- Filter: keep all symbol-table symbols (atoms emit their name as the bare `<name>` — MipsAtom_ macros strip the `code_` prefix).
|
||||||
if not b1 or not b2 then return nil end
|
-- The atoms_source_map pass already filters out non-atom symbols via the source-map.txt cross-ref.
|
||||||
return b1:byte() + b2:byte() * 0x100
|
if name and #name > 0 then
|
||||||
end,
|
local st_value = M.read_u32_le(symtab, entry_off + SYM_ST_VALUE)
|
||||||
read_u32_at = function(offset)
|
addrs[name] = { st_value, st_size }
|
||||||
f:seek("set", offset)
|
end
|
||||||
local b1 = f:read(1)
|
end
|
||||||
local b2 = f:read(1)
|
|
||||||
local b3 = f:read(1)
|
|
||||||
local b4 = f:read(1)
|
|
||||||
if not b1 or not b2 or not b3 or not b4 then return nil end
|
|
||||||
return b1:byte() + b2:byte() * 0x100
|
|
||||||
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000
|
|
||||||
end,
|
|
||||||
read_size = function() return file_size end,
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Delegate the header + section walk to E.*.
|
|
||||||
local hdr, hdr_err = E.parse_elf32_headers(adapter)
|
|
||||||
if not hdr then
|
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_nm] header parse failed: %s\n", tostring(hdr_err)))
|
|
||||||
f:close()
|
|
||||||
return addrs
|
|
||||||
end
|
|
||||||
|
|
||||||
local sections, walk_err = E.walk_sections(adapter, hdr)
|
|
||||||
if not sections then
|
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_nm] section walk failed: %s\n", tostring(walk_err)))
|
|
||||||
f:close()
|
|
||||||
return addrs
|
|
||||||
end
|
|
||||||
|
|
||||||
-- E.collect_symbols returns every defined symbol (no binding filter).
|
|
||||||
-- The metaprogram then applies its STB_LOCAL / STB_GLOBAL + size>0 filter, matching `nm`'s default (external symbols only).
|
|
||||||
local symbols, sym_err = E.collect_symbols(adapter, sections)
|
|
||||||
if not symbols then
|
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_nm] symbol collection failed: %s\n", tostring(sym_err)))
|
|
||||||
f:close()
|
|
||||||
return addrs
|
|
||||||
end
|
|
||||||
|
|
||||||
f:close()
|
|
||||||
|
|
||||||
for name, entry in pairs(symbols) do
|
|
||||||
-- High nibble of st_info = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2).
|
|
||||||
-- math.floor(/16) is portable across LuaJIT 2.0/2.1 and plain Lua 5.x.
|
|
||||||
local binding = math.floor(entry.info / 16)
|
|
||||||
if (binding == 0 or binding == 1) and entry.size > 0 then
|
|
||||||
addrs[name] = { entry.value, entry.size }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -838,11 +822,12 @@ end
|
|||||||
--- * The `.debug_line` section may contain MULTIPLE line-program units
|
--- * The `.debug_line` section may contain MULTIPLE line-program units
|
||||||
--- File indices are 1-based, **per unit**; we concatenate all units and the index ranges from 1..N₁ in unit 1, N₁+1..N₁+N₂ in unit 2, etc.
|
--- File indices are 1-based, **per unit**; we concatenate all units and the index ranges from 1..N₁ in unit 1, N₁+1..N₁+N₂ in unit 2, etc.
|
||||||
--- Per-unit indices (the way gcc emits them, and the way `DW_LNS_set_file` references them in the line program)
|
--- Per-unit indices (the way gcc emits them, and the way `DW_LNS_set_file` references them in the line program)
|
||||||
--- are returned via the `basename_to_index` map only when the unit boundary happens to align with the metaprogram's per-atom `inv.call_file`
|
--- are returned via the `basename_to_index` map only when the unit boundary happens to align with the metaprogram's per-atom
|
||||||
|
--- `inv.call_file` (true today for hello_joypad — the C unit is the LAST unit, and atom-side file indices fit 1-based).
|
||||||
--- * Per spec, the `.debug_line_str` section (DWARF5 §7.5.6) holds the strings referenced by `DW_FORM_line_strp`.
|
--- * Per spec, the `.debug_line_str` section (DWARF5 §7.5.6) holds the strings referenced by `DW_FORM_line_strp`.
|
||||||
--- The legacy DWARF3 format embeds strings directly with null terminators. This helper handles BOTH.
|
--- The legacy DWARF3 format embeds strings directly with null terminators. This helper handles BOTH.
|
||||||
--- * File entries may have multiple forms (gcc -gdwarf-5 with `DW_LNCT_directory_index` emits 2 forms: path + dir_index).
|
--- * File entries may have multiple forms (gcc -gdwarf-5 with `DW_LNCT_directory_index`
|
||||||
--- The helper supports:
|
--- emits 2 forms: path + dir_index). The helper supports:
|
||||||
--- - DW_FORM_line_strp (DWARF5; offset into .debug_line_str)
|
--- - DW_FORM_line_strp (DWARF5; offset into .debug_line_str)
|
||||||
--- - DW_FORM_string (DWARF4-compat; inline null-terminated in .debug_line)
|
--- - DW_FORM_string (DWARF4-compat; inline null-terminated in .debug_line)
|
||||||
--- - DW_FORM_udata (ULEB128)
|
--- - DW_FORM_udata (ULEB128)
|
||||||
@@ -852,7 +837,9 @@ end
|
|||||||
---
|
---
|
||||||
--- Behavior on failure: writes to stderr and returns nil.
|
--- Behavior on failure: writes to stderr and returns nil.
|
||||||
--- Helpers consumed by `passes/dwarf_injection.lua::init_file_index_lookup(elf_path)` calls this once at pass start to populate the module-level `basename_to_index` map;
|
--- Helpers consumed by `passes/dwarf_injection.lua::init_file_index_lookup(elf_path)` calls this once at pass start to populate the module-level `basename_to_index` map;
|
||||||
--- downstream `resolve_provenance_file_index(path)` consumers consult the map directly.
|
--- downstream `resolve_provenance_file_index(path)` consumers
|
||||||
|
--- (which replaced the former hardcoded `ATOM_SOURCE_FILE_INDEX` + `PROVENANCE_BASENAME_TO_FILE_INDEX` table per `conductor/tracks/dwarf_file_index_lookup_20260731/`)
|
||||||
|
--- consult the map directly.
|
||||||
---
|
---
|
||||||
--- @param elf_path string -- absolute path to the post-link ELF (typically the gcc-emitted `.elf` BEFORE dwarf_injector's splice; both shapes work since the splice preserves `.debug_line`)
|
--- @param elf_path string -- absolute path to the post-link ELF (typically the gcc-emitted `.elf` BEFORE dwarf_injector's splice; both shapes work since the splice preserves `.debug_line`)
|
||||||
--- @return table|nil, table|nil, table|nil
|
--- @return table|nil, table|nil, table|nil
|
||||||
|
|||||||
@@ -1,41 +1,77 @@
|
|||||||
# scripts/launch_pcsx_debug.ps1
|
# scripts/launch_pcsx_debug.ps1
|
||||||
#
|
#
|
||||||
# One-shot launcher for debug sessions: starts pcsx-redux with the .ps-exe
|
# One-shot launcher for debug sessions: starts pcsx-redux with the .ps-exe
|
||||||
# loaded, the gdb stub enabled, AND the pcsx_debug_helper Lua plugin loaded
|
# loaded, the gdb stub enabled, the web server enabled, AND the
|
||||||
# so external CLI tools (gdb's `shell` command, etc.)
|
# pcsx_debug_helper Lua plugin loaded so external CLI tools can drive
|
||||||
# can read GTE state via http://localhost:8080/api/v1/lua/gte
|
# reloads via http://localhost:8080/api/v1/lua/reload.
|
||||||
# (the gdb stub doesn't expose COP2 at all).
|
|
||||||
#
|
|
||||||
# usage:
|
|
||||||
# .\scripts\launch_pcsx_debug.ps1
|
|
||||||
# .\scripts\launch_pcsx_debug.ps1 -ExePath build\hello_gte.ps-exe
|
|
||||||
# .\scripts\launch_pcsx_debug.ps1 -HelperZip scripts\pcsx_debug_helper.zip
|
|
||||||
#
|
#
|
||||||
# After launch:
|
# After launch:
|
||||||
# - gdb: target remote localhost:3333
|
# - gdb: target remote localhost:3333
|
||||||
# - web: curl http://localhost:8080/api/v1/lua/gte
|
# - web: POST http://localhost:8080/api/v1/lua/reload?mode=prime&...
|
||||||
|
#
|
||||||
|
# usage:
|
||||||
|
# .\scripts\launch_pcsx_debug.ps1
|
||||||
|
# .\scripts\launch_pcsx_debug.ps1 -ExePath build\hello_camera.ps-exe
|
||||||
|
# .\scripts\launch_pcsx_debug.ps1 -Cpu dynarec
|
||||||
|
# .\scripts\launch_pcsx_debug.ps1 -ElfPath build\hello_camera.elf
|
||||||
#
|
#
|
||||||
# Companion: scripts/debug_psyq.ps1 (bare launch — no .ps-exe, no helper).
|
# Companion: scripts/debug_psyq.ps1 (bare launch — no .ps-exe, no helper).
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$PcsxPath = (Join-Path $PSScriptRoot '..\toolchain\pcsx-redux\vsprojects\x64\Release\pcsx-redux.exe'),
|
[string]$PcsxPath = (Join-Path $PSScriptRoot '..\toolchain\pcsx-redux\vsprojects\x64\Release\pcsx-redux.exe'),
|
||||||
[string]$ExePath = (Join-Path $PSScriptRoot '..\build\hello_gte.ps-exe'),
|
[string]$ExePath = (Join-Path $PSScriptRoot '..\build\hello_camera.ps-exe'),
|
||||||
|
[string]$ElfPath = '',
|
||||||
[string]$HelperZip = (Join-Path $PSScriptRoot 'pcsx_debug_helper.zip'),
|
[string]$HelperZip = (Join-Path $PSScriptRoot 'pcsx_debug_helper.zip'),
|
||||||
[int] $GdbPort = 3333,
|
[int] $GdbPort = 3333,
|
||||||
[int] $WebPort = 8080
|
[int] $WebPort = 8080,
|
||||||
|
[ValidateSet('interpreter', 'dynarec')][string]$Cpu = 'interpreter'
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# ── Derive -ElfPath when absent ──
|
||||||
|
# Convention: the .elf sits beside the .ps-exe with the same stem.
|
||||||
|
if ([string]::IsNullOrEmpty($ElfPath)) {
|
||||||
|
$exeFull = [System.IO.Path]::GetFullPath($ExePath)
|
||||||
|
$stem = [System.IO.Path]::GetFileNameWithoutExtension($exeFull)
|
||||||
|
$exeDir = [System.IO.Path]::GetDirectoryName($exeFull)
|
||||||
|
$ElfPath = Join-Path $exeDir "$stem.elf"
|
||||||
|
}
|
||||||
|
|
||||||
# ── Pre-checks ──
|
# ── Pre-checks ──
|
||||||
foreach ($p in @($PcsxPath, $ExePath, $HelperZip)) {
|
foreach ($p in @($PcsxPath, $ExePath, $ElfPath, $HelperZip)) {
|
||||||
if (-not (Test-Path $p)) {
|
if (-not (Test-Path -LiteralPath $p)) {
|
||||||
Write-Error "Missing: $p"
|
Write-Error "Missing: $p"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Reject a stale helper zip (Task 8) ──
|
||||||
|
# The helper zip must be newer than every .lua source that contributes
|
||||||
|
# to it. A stale zip means the running plugin does not match the on-disk
|
||||||
|
# source, which makes the reload contract meaningless.
|
||||||
|
$helperDir = Join-Path $PSScriptRoot 'pcsx_debug_helper'
|
||||||
|
$elf32Src = Join-Path $PSScriptRoot 'elf32.lua'
|
||||||
|
$sourceLuas = @(
|
||||||
|
(Join-Path $helperDir 'autoexec.lua'),
|
||||||
|
(Join-Path $helperDir 'reload.lua'),
|
||||||
|
$elf32Src
|
||||||
|
) | Where-Object { Test-Path -LiteralPath $_ }
|
||||||
|
|
||||||
|
$zipTime = (Get-Item -LiteralPath $HelperZip).LastWriteTime
|
||||||
|
$stale = $false
|
||||||
|
foreach ($src in $sourceLuas) {
|
||||||
|
$srcTime = (Get-Item -LiteralPath $src).LastWriteTime
|
||||||
|
if ($srcTime -gt $zipTime) {
|
||||||
|
Write-Error "helper zip is older than source: $src (zip=$($zipTime.ToString('o')) src=$($srcTime.ToString('o')); rerun build_psyq.ps1 to regenerate."
|
||||||
|
$stale = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($stale) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# Kill any existing pcsx-redux so the archive file isn't locked.
|
# Kill any existing pcsx-redux so the archive file isn't locked.
|
||||||
Get-Process pcsx-redux -ErrorAction SilentlyContinue | Stop-Process -Force
|
Get-Process pcsx-redux -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||||
Start-Sleep -Seconds 2
|
Start-Sleep -Seconds 2
|
||||||
@@ -44,17 +80,23 @@ Start-Sleep -Seconds 2
|
|||||||
$absExe = [System.IO.Path]::GetFullPath($ExePath)
|
$absExe = [System.IO.Path]::GetFullPath($ExePath)
|
||||||
$absZip = [System.IO.Path]::GetFullPath($HelperZip)
|
$absZip = [System.IO.Path]::GetFullPath($HelperZip)
|
||||||
|
|
||||||
|
$cpuFlag = if ($Cpu -eq 'dynarec') { '-dynarec' } else { '-interpreter' }
|
||||||
|
|
||||||
$args = @(
|
$args = @(
|
||||||
'-gdb', '-run'
|
'-gdb', '-run'
|
||||||
'-loadexe', "`"$absExe`""
|
'-loadexe', "`"$absExe`""
|
||||||
'-archive', "`"$absZip`""
|
'-archive', "`"$absZip`""
|
||||||
|
'-webserver'
|
||||||
|
$cpuFlag
|
||||||
)
|
)
|
||||||
|
|
||||||
Write-Host "Launching pcsx-redux..." -ForegroundColor Cyan
|
Write-Host "Launching pcsx-redux..." -ForegroundColor Cyan
|
||||||
Write-Host " ps-exe : $absExe"
|
Write-Host " ps-exe : $absExe"
|
||||||
|
Write-Host " elf : $ElfPath"
|
||||||
Write-Host " helper zip: $absZip"
|
Write-Host " helper zip: $absZip"
|
||||||
Write-Host " gdb : localhost:$GdbPort"
|
Write-Host " gdb : localhost:$GdbPort"
|
||||||
Write-Host " web : localhost:$WebPort/api/v1/lua/gte"
|
Write-Host " web : localhost:$WebPort/api/v1/lua/reload"
|
||||||
|
Write-Host " cpu : $Cpu ($cpuFlag)"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
Start-Process -FilePath $PcsxPath -ArgumentList $args | Out-Null
|
Start-Process -FilePath $PcsxPath -ArgumentList $args | Out-Null
|
||||||
@@ -89,6 +131,44 @@ try {
|
|||||||
Write-Host "Check the pcsx-redux Lua Console for debug cli messages." -ForegroundColor Yellow
|
Write-Host "Check the pcsx-redux Lua Console for debug cli messages." -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Prime the reload handler (Task 8) ──
|
||||||
|
# The reload handler keeps an internal ACTIVE manifest of the running
|
||||||
|
# ELF; reload requests fail with reload_not_primed until prime succeeds.
|
||||||
|
# We retry until the response carries ok=true or the launch deadline
|
||||||
|
# expires — the helper may not have finished registering handlers in the
|
||||||
|
# first web-poll cycle after the gte handler comes up.
|
||||||
|
$absElf = [System.IO.Path]::GetFullPath($ElfPath)
|
||||||
|
$encodedPath = [uri]::EscapeDataString($absElf)
|
||||||
|
$primeUri = "http://localhost:${WebPort}/api/v1/lua/reload?mode=prime&target=hello_camera&path=${encodedPath}"
|
||||||
|
|
||||||
|
Write-Host "Priming reload handler: $primeUri" -ForegroundColor Cyan
|
||||||
|
$primeDeadline = (Get-Date).AddSeconds(15)
|
||||||
|
$primeOk = $false
|
||||||
|
while ((Get-Date) -lt $primeDeadline) {
|
||||||
|
try {
|
||||||
|
$resp = Invoke-WebRequest -Method Post -Uri $primeUri -UseBasicParsing -TimeoutSec 5
|
||||||
|
$body = if ($resp.Content -is [byte[]]) {
|
||||||
|
[System.Text.Encoding]::UTF8.GetString([byte[]]$resp.Content)
|
||||||
|
} else {
|
||||||
|
[string]$resp.Content
|
||||||
|
}
|
||||||
|
$obj = $body | ConvertFrom-Json
|
||||||
|
if ($obj.ok) {
|
||||||
|
Write-Host "Prime OK: $(($obj | ConvertTo-Json -Compress))" -ForegroundColor Green
|
||||||
|
$primeOk = $true
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
Write-Host "Prime not yet ready: error=$($obj.error)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "Prime request failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
}
|
||||||
|
if (-not $primeOk) {
|
||||||
|
Write-Warning "Prime did not return ok=true before the launch deadline. Reload requests will fail until the user primes manually."
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "pcsx-redux running. PIDs:" -ForegroundColor Cyan
|
Write-Host "pcsx-redux running. PIDs:" -ForegroundColor Cyan
|
||||||
Get-Process pcsx-redux | Select-Object Id, ProcessName | Format-Table
|
Get-Process pcsx-redux | Select-Object Id, ProcessName | Format-Table
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# make_helper_zip.ps1
|
||||||
|
#
|
||||||
|
# Regenerate scripts/pcsx_debug_helper.zip from scripts/pcsx_debug_helper/.
|
||||||
|
# The archive contains exactly three entries at archive root:
|
||||||
|
#
|
||||||
|
# autoexec.lua
|
||||||
|
# elf32.lua (copied in from scripts/elf32.lua before packaging)
|
||||||
|
# reload.lua
|
||||||
|
#
|
||||||
|
# Determinism: CreateFromDirectory on the same set of files produces
|
||||||
|
# identical bytes. Verified by running the same command twice and
|
||||||
|
# asserting SHA-256 equality (see plan.md Task 6 Step 4).
|
||||||
|
#
|
||||||
|
# Performance: the implementation uses System.IO.Compression.ZipFile
|
||||||
|
# (BCL, in-process). Benchmarked: ~2 ms cold, ~2 ms warm on this
|
||||||
|
# workstation. Compress-Archive is rejected because its first call
|
||||||
|
# takes ~200 ms (assembly load) and subsequent calls take ~16 ms
|
||||||
|
# (process spawn per invocation). The 50 ms budget documented in
|
||||||
|
# plan.md Task 8 Step 3 excludes the compiler/assembler toolchain.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# pwsh -NoProfile -File scripts\make_helper_zip.ps1
|
||||||
|
#
|
||||||
|
# Optional -OutputPath switches the destination. Default is
|
||||||
|
# scripts/pcsx_debug_helper.zip next to the helper dir.
|
||||||
|
#
|
||||||
|
# Companion: scripts/pcsx_debug_helper/{autoexec,elf32,reload}.lua
|
||||||
|
# tests/reload_helper_zip_regen.ps1 (planned Task 8 verifier)
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$HelperDir = (Join-Path $PSScriptRoot 'pcsx_debug_helper'),
|
||||||
|
[string]$SourcesDir = $PSScriptRoot,
|
||||||
|
[string]$OutputPath = (Join-Path $PSScriptRoot 'pcsx_debug_helper.zip')
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $HelperDir)) {
|
||||||
|
throw "helper dir not found: $HelperDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stage elf32.lua into the helper dir so the in-process ZipFile walker
|
||||||
|
# picks it up alongside the helper-local files. elf32.lua is the shared
|
||||||
|
# ELF32 byte reader; the production reload.lua loads it through
|
||||||
|
# Support.extra.dofile("elf32.lua") at runtime.
|
||||||
|
$elf32Src = Join-Path $SourcesDir 'elf32.lua'
|
||||||
|
$elf32Dest = Join-Path $HelperDir 'elf32.lua'
|
||||||
|
if (-not (Test-Path -LiteralPath $elf32Src)) {
|
||||||
|
throw "elf32.lua not found at $elf32Src"
|
||||||
|
}
|
||||||
|
Copy-Item -LiteralPath $elf32Src -Destination $elf32Dest -Force
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Remove any existing archive so CreateFromDirectory can write fresh.
|
||||||
|
# ZipFile.CreateFromDirectory throws if the destination exists.
|
||||||
|
if (Test-Path -LiteralPath $OutputPath) {
|
||||||
|
Remove-Item -LiteralPath $OutputPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# In-process zip; ~2 ms cold, ~2 ms warm. BCL compression matches
|
||||||
|
# Compress-Archive at CompressionLevel Optimal for these small files.
|
||||||
|
# Assembly is loaded once per pwsh.exe; the first run pays ~14 ms,
|
||||||
|
# subsequent runs pay ~0.2 ms.
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
|
[System.IO.Compression.ZipFile]::CreateFromDirectory(
|
||||||
|
$HelperDir, $OutputPath,
|
||||||
|
[System.IO.Compression.CompressionLevel]::Optimal,
|
||||||
|
$false) | Out-Null
|
||||||
|
|
||||||
|
$sha = (Get-FileHash -LiteralPath $OutputPath -Algorithm SHA256).Hash
|
||||||
|
Write-Output ("[make_helper_zip] wrote {0} bytes, sha256={1}" -f `
|
||||||
|
(Get-Item -LiteralPath $OutputPath).Length, $sha)
|
||||||
|
Write-Output "[make_helper_zip] entries: autoexec.lua, elf32.lua, reload.lua"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
# Remove the staged elf32.lua so the helper directory only contains
|
||||||
|
# the files the user expects to see there.
|
||||||
|
if (Test-Path -LiteralPath $elf32Dest) {
|
||||||
|
Remove-Item -LiteralPath $elf32Dest -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -992,7 +992,7 @@ local function build_dwarf_line_section(existing, atom_table)
|
|||||||
while unit_pos < #existing do
|
while unit_pos < #existing do
|
||||||
if unit_pos + 4 > #existing then return existing end
|
if unit_pos + 4 > #existing then return existing end
|
||||||
local unit_length = elf_dwarf.read_u32_le(existing, unit_pos)
|
local unit_length = elf_dwarf.read_u32_le(existing, unit_pos)
|
||||||
if unit_length == elf_dwarf.dw_dwarf32_terminator then return existing end
|
if unit_length == elf_dwarf.ELF32.dw_dwarf32_terminator then return existing end
|
||||||
local unit_end_excl = unit_pos + 4 + unit_length
|
local unit_end_excl = unit_pos + 4 + unit_length
|
||||||
if unit_end_excl > #existing then return existing end
|
if unit_end_excl > #existing then return existing end
|
||||||
last_pos, last_length, last_end = unit_pos, unit_length, unit_end_excl
|
last_pos, last_length, last_end = unit_pos, unit_length, unit_end_excl
|
||||||
@@ -1053,7 +1053,7 @@ local function build_dwarf_aranges_section(existing, atom_table)
|
|||||||
while i < #existing do
|
while i < #existing do
|
||||||
-- Read this unit's length.
|
-- Read this unit's length.
|
||||||
local ul = elf_dwarf.read_u32_le(existing, i)
|
local ul = elf_dwarf.read_u32_le(existing, i)
|
||||||
if ul == elf_dwarf.dw_dwarf32_terminator then
|
if ul == elf_dwarf.ELF32.dw_dwarf32_terminator then
|
||||||
-- DWARF64 marker - not supported.
|
-- DWARF64 marker - not supported.
|
||||||
io.stderr:write("[dwarf_injection] WARN: .debug_aranges contains a DWARF64 marker (0xFFFFFFFF); the 64-bit extension is not supported by this metaprogram; passing through unchanged\n")
|
io.stderr:write("[dwarf_injection] WARN: .debug_aranges contains a DWARF64 marker (0xFFFFFFFF); the 64-bit extension is not supported by this metaprogram; passing through unchanged\n")
|
||||||
return existing
|
return existing
|
||||||
|
|||||||
Binary file not shown.
@@ -16,36 +16,60 @@
|
|||||||
-- Companion: scripts/gdb/gdb_tape_atoms.gdb (covers GPRs + atom-aware stepping).
|
-- Companion: scripts/gdb/gdb_tape_atoms.gdb (covers GPRs + atom-aware stepping).
|
||||||
|
|
||||||
local function register_handlers()
|
local function register_handlers()
|
||||||
if not PCSX.WebServer then PCSX.WebServer = {} end
|
if not PCSX.WebServer then PCSX.WebServer = {} end
|
||||||
if not PCSX.WebServer.Handlers then PCSX.WebServer.Handlers = {} end
|
if not PCSX.WebServer.Handlers then PCSX.WebServer.Handlers = {} end
|
||||||
|
|
||||||
-- ── GTE state ──
|
-- ── GTE state ──
|
||||||
PCSX.WebServer.Handlers.gte = function(req)
|
PCSX.WebServer.Handlers.gte = function(req)
|
||||||
local r = PCSX.getRegisters()
|
local r = PCSX.getRegisters()
|
||||||
local out = { "pc=0x" .. string.format("%x", r.pc) }
|
local out = { "pc=0x" .. string.format("%x", r.pc) }
|
||||||
for i = 0, 31 do
|
for i = 0, 31 do
|
||||||
out[#out + 1] = string.format("D[%d]=0x%08x C[%d]=0x%08x",
|
out[#out + 1] = string.format("D[%d]=0x%08x C[%d]=0x%08x",
|
||||||
i, r.CP2D.r[i], i, r.CP2C.r[i])
|
i, r.CP2D.r[i], i, r.CP2C.r[i])
|
||||||
end
|
end
|
||||||
return table.concat(out, "\n")
|
return table.concat(out, "\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ── GP state (pointer to existing endpoints) ──
|
-- ── GP state (pointer to existing endpoints) ──
|
||||||
-- pcsx-redux's Lua GPU API exposes only takeScreenShot(); no GPUSTAT / GP0 / GP1 command log / display state.
|
-- pcsx-redux's Lua GPU API exposes only takeScreenShot(); no GPUSTAT / GP0 / GP1 command log / display state.
|
||||||
-- We point to the existing web endpoints that DO expose those (when the emulator is actually rendering. Paused-at-BP frames won't have a fresh frame).
|
-- We point to the existing web endpoints that DO expose those (when the emulator is actually rendering. Paused-at-BP frames won't have a fresh frame).
|
||||||
PCSX.WebServer.Handlers.gp = function(req)
|
PCSX.WebServer.Handlers.gp = function(req)
|
||||||
local out = {
|
local out = {
|
||||||
"gpu_screenshot_png=http://localhost:8080/api/v1/state/still",
|
"gpu_screenshot_png=http://localhost:8080/api/v1/state/still",
|
||||||
"vram_raw=http://localhost:8080/api/v1/gpu/vram/raw (1MB VRAM)",
|
"vram_raw=http://localhost:8080/api/v1/gpu/vram/raw (1MB VRAM)",
|
||||||
"gpustat=NOT_AVAILABLE_VIA_LUA",
|
"gpustat=NOT_AVAILABLE_VIA_LUA",
|
||||||
"gp_command_log=NOT_AVAILABLE_VIA_LUA (use pcsx-redux Debug > GPU Logger)",
|
"gp_command_log=NOT_AVAILABLE_VIA_LUA (use pcsx-redux Debug > GPU Logger)",
|
||||||
"hint_run_emulator_unpaused_for_screenshot",
|
"hint_run_emulator_unpaused_for_screenshot",
|
||||||
}
|
}
|
||||||
return table.concat(out, "\n")
|
return table.concat(out, "\n")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, err = pcall(register_handlers)
|
local ok, err = pcall(register_handlers)
|
||||||
if ok then print("[pcsx_debug_helper] handlers registered: gte, gp")
|
if ok then print("[pcsx_debug_helper] handlers registered: gte, gp")
|
||||||
else print("[pcsx_debug_helper] registration failed: " .. tostring(err))
|
else print("[pcsx_debug_helper] registration failed: " .. tostring(err))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ── reload handler (Task 6) ──
|
||||||
|
-- After gte and gp register successfully, load reload.lua through Support.extra.dofile and call its install(pcsx, support).
|
||||||
|
-- The whole sequence runs inside pcall so a missing zip, missing module table,
|
||||||
|
-- or throwing install never disturbs the gte and gp handlers already registered above (handler isolation).
|
||||||
|
--
|
||||||
|
-- The failure messages are intentionally single-line so the helper's boot log stays scannable.
|
||||||
|
if type(Support) == "table"
|
||||||
|
and type(Support.extra) == "table"
|
||||||
|
and type(Support.extra.dofile) == "function" then
|
||||||
|
local load_ok, reload_mod = pcall(Support.extra.dofile, "reload.lua")
|
||||||
|
if load_ok and type(reload_mod) == "table" and type(reload_mod.install) == "function" then
|
||||||
|
local install_ok, install_err = pcall(reload_mod.install, PCSX, Support)
|
||||||
|
if install_ok then
|
||||||
|
print("[pcsx_debug_helper] reload handler registered")
|
||||||
|
else
|
||||||
|
print("[pcsx_debug_helper] reload registration failed: " .. tostring(install_err))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
print("[pcsx_debug_helper] reload load failed: " .. tostring(reload_mod))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
print("[pcsx_debug_helper] reload load failed: Support.extra.dofile unavailable")
|
||||||
|
end
|
||||||
|
|||||||
@@ -0,0 +1,902 @@
|
|||||||
|
-- reload.lua - Side-effect-free hot-reload helper for the
|
||||||
|
-- pcsx_redux_hot_reload track (Task 2). This file owns the HTTP request
|
||||||
|
-- surface that the launch / reload client targets:
|
||||||
|
--
|
||||||
|
-- POST /api/v1/lua/reload?mode=prime&target=hello_camera&path=<encoded-elf>
|
||||||
|
-- POST /api/v1/lua/reload?mode=elf&target=hello_camera&path=<encoded-elf>
|
||||||
|
-- POST /api/v1/lua/reload?mode=patch&target=hello_camera&addr=...&hex=...
|
||||||
|
--
|
||||||
|
-- This module exposes the public surface used by the contract harness
|
||||||
|
-- (tests/reload_helper_contract.lua) and the runtime installed by
|
||||||
|
-- scripts/pcsx_debug_helper/autoexec.lua. The module must not reference
|
||||||
|
-- the global PCSX table at load time; the host is passed in explicitly
|
||||||
|
-- through M.new(host) and M.install(pcsx, support).
|
||||||
|
--
|
||||||
|
-- Public surface:
|
||||||
|
-- M.parse_query(query) -> table, nil OR nil, err_string
|
||||||
|
-- M.json_response(fields) -> string (sorted keys)
|
||||||
|
-- M.parse_manifest(...) -> Task 3 (real impl uses elf32.lua)
|
||||||
|
-- M.new(host) -> runtime object (Task 4; stub here)
|
||||||
|
-- M.install(pcsx, support) -> registers web handler (Task 6; stub here)
|
||||||
|
--
|
||||||
|
-- Companion: scripts/pcsx_debug_helper/autoexec.lua.
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Load the shared ELF32 helpers.
|
||||||
|
--
|
||||||
|
-- **The bane of this refactor:** the helper VM (PCSX-Redux) does not expose
|
||||||
|
-- `require` for paths outside the helper zip. The production loader is
|
||||||
|
-- `Support.extra.dofile("elf32.lua")` — Support.extra.dofile resolves the
|
||||||
|
-- name against the helper zip's contents (the zip is generated by the
|
||||||
|
-- build script and includes both `reload.lua` and `elf32.lua` after Task 6).
|
||||||
|
--
|
||||||
|
-- The test harness at `tests/reload_helper_contract.lua` loads `reload.lua`
|
||||||
|
-- via standard Lua `dofile` with an absolute path; it does not install a
|
||||||
|
-- `Support` object. We detect the runtime context: if `Support.extra.dofile`
|
||||||
|
-- exists, use it (production path); otherwise fall back to standard `dofile`
|
||||||
|
-- with an absolute path (test harness path).
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
local function load_elf32()
|
||||||
|
if type(Support) == "table"
|
||||||
|
and type(Support.extra) == "table"
|
||||||
|
and type(Support.extra.dofile) == "function" then
|
||||||
|
return Support.extra.dofile("elf32.lua")
|
||||||
|
end
|
||||||
|
-- Test harness + any other context that supplies standard Lua dofile.
|
||||||
|
return dofile("C:/projects/Pikuma/ps1/scripts/elf32.lua")
|
||||||
|
end
|
||||||
|
|
||||||
|
local E = load_elf32()
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- parse_query(query)
|
||||||
|
--
|
||||||
|
-- Parses an application/x-www-form-urlencoded query string into a table.
|
||||||
|
--
|
||||||
|
-- Rules (per spec §8 + plan.md Task 2 Step 3):
|
||||||
|
-- * Each pair is split on the first '='; the key is to the left, the value
|
||||||
|
-- to the right. A pair without '=' is a malformed_pair.
|
||||||
|
-- * Percent escapes '%HH' (HH = two hex digits) decode to the corresponding
|
||||||
|
-- byte. A '%' not followed by two hex digits is a malformed_escape.
|
||||||
|
-- * '+' decodes to a literal space (applied after percent decode).
|
||||||
|
-- * A key appearing more than once is a duplicate_key error.
|
||||||
|
--
|
||||||
|
-- Returns the parsed table on success. On failure returns nil and a stable
|
||||||
|
-- error string suitable for the JSON error envelope. An empty / nil query
|
||||||
|
-- returns an empty table (not an error).
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
local function percent_decode(s)
|
||||||
|
-- Walk the string once, byte by byte. A '%' must be followed by exactly
|
||||||
|
-- two hex digits; '+' decodes to ' '; everything else is passed through.
|
||||||
|
local out = {}
|
||||||
|
local i = 1
|
||||||
|
local len = #s
|
||||||
|
while i <= len do
|
||||||
|
local c = s:sub(i, i)
|
||||||
|
if c == "%" then
|
||||||
|
if i + 2 > len then
|
||||||
|
return nil -- truncated escape (e.g., '%' at end or '%X')
|
||||||
|
end
|
||||||
|
local hex = s:sub(i + 1, i + 2)
|
||||||
|
local hd1, hd2 = hex:sub(1, 1), hex:sub(2, 2)
|
||||||
|
-- Validate both characters are hex digits.
|
||||||
|
if not (hd1:match("[0-9A-Fa-f]") and hd2:match("[0-9A-Fa-f]")) then
|
||||||
|
return nil -- malformed escape
|
||||||
|
end
|
||||||
|
out[#out + 1] = string.char(tonumber(hex, 16))
|
||||||
|
i = i + 3
|
||||||
|
else
|
||||||
|
out[#out + 1] = c
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.concat(out)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function plus_to_space(s)
|
||||||
|
-- Standalone helper so callers can decode '+' after percent decoding.
|
||||||
|
return (s:gsub("+", " "))
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.parse_query(query)
|
||||||
|
if query == nil or query == "" then
|
||||||
|
return {}, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = {}
|
||||||
|
local seen = {}
|
||||||
|
|
||||||
|
for pair in query:gmatch("[^&]+") do
|
||||||
|
-- Split on the first '=' only.
|
||||||
|
local eq = pair:find("=", 1, true)
|
||||||
|
if not eq then
|
||||||
|
return nil, "malformed_pair"
|
||||||
|
end
|
||||||
|
local raw_key = pair:sub(1, eq - 1)
|
||||||
|
local raw_value = pair:sub(eq + 1)
|
||||||
|
|
||||||
|
-- Percent-decode first, then convert '+' to space. The order matters:
|
||||||
|
-- a '%2B' should decode to '+' (literal plus), not be re-converted to a
|
||||||
|
-- space. Per RFC 1866 §8.2.1, '+' is a literal plus in the encoded form
|
||||||
|
-- only when it represents a space.
|
||||||
|
local key = percent_decode(raw_key)
|
||||||
|
if key == nil then
|
||||||
|
return nil, "malformed_escape"
|
||||||
|
end
|
||||||
|
key = plus_to_space(key)
|
||||||
|
|
||||||
|
local val = percent_decode(raw_value)
|
||||||
|
if val == nil then
|
||||||
|
return nil, "malformed_escape"
|
||||||
|
end
|
||||||
|
val = plus_to_space(val)
|
||||||
|
|
||||||
|
if seen[key] then
|
||||||
|
return nil, "duplicate_key"
|
||||||
|
end
|
||||||
|
seen[key] = true
|
||||||
|
|
||||||
|
result[key] = val
|
||||||
|
end
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- json_response(fields)
|
||||||
|
--
|
||||||
|
-- Deterministic JSON object encoder. Returns a string. Keys are sorted
|
||||||
|
-- alphabetically before emission so byte-for-byte equality is testable
|
||||||
|
-- across runs and across PS1 captures.
|
||||||
|
--
|
||||||
|
-- Supported value types: string, number, boolean, nil (encoded as null).
|
||||||
|
-- Strings escape '\', '"', and the C0 control range (0x00..0x1F). The
|
||||||
|
-- named escapes use the conventional single-char forms: \\, \", \b, \f,
|
||||||
|
-- \n, \r, \t. Everything else in 0x00..0x1F is \uXXXX.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
local function json_escape_string(s)
|
||||||
|
-- Two passes: first the named escapes, then the catch-all C0 range
|
||||||
|
-- (%c covers 0x00..0x1F in Lua patterns). Using plain string.gsub
|
||||||
|
-- with a literal replacement table covers the named escapes; a
|
||||||
|
-- second gsub handles the rest.
|
||||||
|
s = s:gsub('[\\"]', {
|
||||||
|
["\\"] = "\\\\",
|
||||||
|
['"'] = '\\"',
|
||||||
|
})
|
||||||
|
s = s:gsub("\b", "\\b")
|
||||||
|
s = s:gsub("\f", "\\f")
|
||||||
|
s = s:gsub("\n", "\\n")
|
||||||
|
s = s:gsub("\r", "\\r")
|
||||||
|
s = s:gsub("\t", "\\t")
|
||||||
|
-- Remaining C0 control characters (0x00..0x1F) become \uXXXX. We
|
||||||
|
-- intentionally keep the named escapes above (which are already
|
||||||
|
-- single backslashes in the output) from being re-escaped: gsub on
|
||||||
|
-- the literal control char bytes doesn't match the backslashes we
|
||||||
|
-- already inserted.
|
||||||
|
s = s:gsub("([%c])", function(c)
|
||||||
|
return string.format("\\u%04x", string.byte(c))
|
||||||
|
end)
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.json_response(fields)
|
||||||
|
if type(fields) ~= "table" then
|
||||||
|
error("json_response: expected table, got " .. type(fields))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sort keys for deterministic output. Lua's table.sort is byte-wise
|
||||||
|
-- and stable for strings; JSON object key order is not significant
|
||||||
|
-- but tests rely on a fixed order to compare against fixtures.
|
||||||
|
local keys = {}
|
||||||
|
for k in pairs(fields) do
|
||||||
|
keys[#keys + 1] = k
|
||||||
|
end
|
||||||
|
table.sort(keys)
|
||||||
|
|
||||||
|
local parts = {}
|
||||||
|
parts[#parts + 1] = "{"
|
||||||
|
for i = 1, #keys do
|
||||||
|
local k = keys[i]
|
||||||
|
if i > 1 then
|
||||||
|
parts[#parts + 1] = ","
|
||||||
|
end
|
||||||
|
parts[#parts + 1] = '"'
|
||||||
|
parts[#parts + 1] = json_escape_string(k)
|
||||||
|
parts[#parts + 1] = '":'
|
||||||
|
local v = fields[k]
|
||||||
|
local tv = type(v)
|
||||||
|
if tv == "string" then
|
||||||
|
parts[#parts + 1] = '"'
|
||||||
|
parts[#parts + 1] = json_escape_string(v)
|
||||||
|
parts[#parts + 1] = '"'
|
||||||
|
elseif tv == "number" then
|
||||||
|
parts[#parts + 1] = tostring(v)
|
||||||
|
elseif tv == "boolean" then
|
||||||
|
parts[#parts + 1] = v and "true" or "false"
|
||||||
|
elseif v == nil then
|
||||||
|
parts[#parts + 1] = "null"
|
||||||
|
else
|
||||||
|
error("json_response: unsupported value type " .. tv .. " for key " .. tostring(k))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
parts[#parts + 1] = "}"
|
||||||
|
return table.concat(parts)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- ELF32 manifest parser (Task 3).
|
||||||
|
--
|
||||||
|
-- Parses a little-endian ELF32 file exposed through a file_adapter that
|
||||||
|
-- provides read_u8_at/read_u16_at/read_u32_at/read_size. The parser validates the
|
||||||
|
-- magic, class, data encoding, and machine before reading anything else.
|
||||||
|
-- It resolves section names through the .shstrtab table and symbols
|
||||||
|
-- through every SHT_SYMTAB section (and its linked string table).
|
||||||
|
--
|
||||||
|
-- The output manifest contains the state ABI the reload gate must
|
||||||
|
-- preserve plus the addresses the helper writes to the CPU on a reload.
|
||||||
|
-- Loaded sections (SHF_ALLOC, non-SHT_NOBITS) are recorded so the runtime
|
||||||
|
-- can reject any ELF whose loaded range overlaps the preserved smem.
|
||||||
|
--
|
||||||
|
-- **Refactor:** the format-constant tables + the byte-level walker live in
|
||||||
|
-- scripts/elf32.lua (loaded above via `load_elf32()`). This module retains
|
||||||
|
-- only the manifest-specific validation: required symbols, smem size, stack
|
||||||
|
-- alignment, loaded-section overlap. The net effect is ~80 lines shorter.
|
||||||
|
--
|
||||||
|
-- Stable error codes (returned as the second value):
|
||||||
|
-- bad_magic, unsupported_elf_class, unsupported_elf_data,
|
||||||
|
-- non_mips_machine, truncated_header, truncated_section_headers,
|
||||||
|
-- missing_shstrtab, missing_symtab_strtab, missing_smem,
|
||||||
|
-- missing_data_start, missing_data_end, missing_bss_start,
|
||||||
|
-- missing_bss_end, missing_stack_top, missing_hot_reload_entry,
|
||||||
|
-- zero_smem_size, stack_misaligned, stack_out_of_main_ram,
|
||||||
|
-- section_overlaps_smem, bad_file_adapter
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Convert a KSEG0/KSEG1/physical address to its physical main-RAM offset.
|
||||||
|
local function to_physical(addr)
|
||||||
|
if addr >= 0x80000000 and addr < 0x80200000 then
|
||||||
|
return addr - 0x80000000
|
||||||
|
elseif addr >= 0xa0000000 and addr < 0xa0200000 then
|
||||||
|
return addr - 0xa0000000
|
||||||
|
end
|
||||||
|
return addr
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Strip KSEG0 / KSEG1 alias from an address and return the physical main-RAM
|
||||||
|
-- offset. Used by M.elf_reload and M.patch_handler. Returns nil when the
|
||||||
|
-- address falls outside physical main RAM (0..0x1fffff), KSEG0 main RAM
|
||||||
|
-- (0x80000000..0x801fffff), or KSEG1 main RAM (0xa0000000..0xa01fffff).
|
||||||
|
-- Per spec §7 the patch path MUST reject scratchpad (0x1F800000+), BIOS
|
||||||
|
-- (0x1FC00000+), MMIO, and expansion aliases; this helper centralizes the
|
||||||
|
-- strip + range check so callers cannot forget the upper bound.
|
||||||
|
local function strip_kseg(addr)
|
||||||
|
if type(addr) ~= "number" then return nil end
|
||||||
|
if addr >= 0x80000000 and addr < 0x80200000 then
|
||||||
|
return addr - 0x80000000
|
||||||
|
elseif addr >= 0xa0000000 and addr < 0xa0200000 then
|
||||||
|
return addr - 0xa0000000
|
||||||
|
elseif addr >= 0 and addr < 0x200000 then
|
||||||
|
return addr
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse a hex string ("0xHHHH..." or "HHHH...") into a 32-bit unsigned
|
||||||
|
-- integer. Returns nil + stable error on absent / non-hex / out-of-range.
|
||||||
|
-- Used for both the patch path's addr/hex query parameters and any other
|
||||||
|
-- 32-bit hex field the API may add. Accepts up to 8 hex digits.
|
||||||
|
local function parse_hex_u32(s, missing_err, badhex_err)
|
||||||
|
if type(s) ~= "string" or #s == 0 then
|
||||||
|
return nil, missing_err or "missing_hex"
|
||||||
|
end
|
||||||
|
local clean = s:match("^0[xX]([0-9A-Fa-f]+)$")
|
||||||
|
or s:match("^([0-9A-Fa-f]+)$")
|
||||||
|
if not clean then return nil, badhex_err or "non_hex" end
|
||||||
|
if #clean > 8 then return nil, badhex_err or "non_hex" end
|
||||||
|
return tonumber(clean, 16), nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Trap on a missing E.* — keeps the existing one-line-error pattern when
|
||||||
|
-- the helper zip is stale or absent.
|
||||||
|
local function stack()
|
||||||
|
io.stderr:write("[reload.parse_manifest] FATAL: scripts/elf32.lua not loaded; aborting\n")
|
||||||
|
error("elf32 module not loaded")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_manifest_impl(file_adapter, target, path, require_entry)
|
||||||
|
-- Wrap the body in a pcall so any thrown exception (e.g. a bad
|
||||||
|
-- adapter method or a malformed section header) surfaces as a
|
||||||
|
-- parse_error with the message and traceback instead of being lost
|
||||||
|
-- into the with_busy_guard xpcall as a generic internal_error.
|
||||||
|
local inner_ok, inner_result, inner_err = pcall(function()
|
||||||
|
-- Validate the adapter surface. E.validate_adapter returns the same
|
||||||
|
-- "bad_file_adapter" error code the prior implementation used.
|
||||||
|
local ok, err = E.validate_adapter(file_adapter)
|
||||||
|
if not ok then return nil, err end
|
||||||
|
|
||||||
|
-- Magic, class, data encoding. E.parse_elf32_headers reads fields at
|
||||||
|
-- the wire offsets specified in E.ELF32_HEADER.
|
||||||
|
local hdr, hdr_err = E.parse_elf32_headers(file_adapter)
|
||||||
|
if not hdr then return nil, hdr_err end
|
||||||
|
|
||||||
|
-- Machine check (e.g. EM_MIPS = 8). e_machine is at offset 0x12 (18).
|
||||||
|
-- The reload helper rejects non-MIPS ELFs before any symbol work.
|
||||||
|
-- Explicit pass style: E.read_u16(adapter, off). The helper wraps the
|
||||||
|
-- Support.File adapter once to strip its implicit `self` so the
|
||||||
|
-- parser shape stays flat-function, not colon-dispatch.
|
||||||
|
local machine = E.read_u16(file_adapter, 0x12)
|
||||||
|
if not machine then return nil, "truncated_header" end
|
||||||
|
if machine ~= E.EM_MIPS then
|
||||||
|
return nil, "non_mips_machine"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Walk sections. E.walk_sections also resolves .shstrtab names.
|
||||||
|
local sections, walk_err = E.walk_sections(file_adapter, hdr)
|
||||||
|
if not sections then return nil, walk_err end
|
||||||
|
|
||||||
|
-- Walk symbols. E.collect_symbols includes both STB_LOCAL and STB_GLOBAL
|
||||||
|
-- (the live ELF stores smem as a local symbol).
|
||||||
|
local symbols, sym_err = E.collect_symbols(file_adapter, sections)
|
||||||
|
if not symbols then return nil, sym_err end
|
||||||
|
|
||||||
|
-- Required symbols.
|
||||||
|
local smem = symbols["smem"]
|
||||||
|
local data_start = symbols["__data_start"]
|
||||||
|
local data_end = symbols["__data_end"]
|
||||||
|
local bss_start = symbols["__bss_start"]
|
||||||
|
local bss_end = symbols["__bss_end"]
|
||||||
|
local stack_top_s = symbols["__sp"]
|
||||||
|
local entry_s = symbols["hot_reload_entry"]
|
||||||
|
|
||||||
|
if not smem then return nil, "missing_smem" end
|
||||||
|
if not data_start then return nil, "missing_data_start" end
|
||||||
|
if not data_end then return nil, "missing_data_end" end
|
||||||
|
if not bss_start then return nil, "missing_bss_start" end
|
||||||
|
if not bss_end then return nil, "missing_bss_end" end
|
||||||
|
if not stack_top_s then return nil, "missing_stack_top" end
|
||||||
|
if require_entry and not entry_s then
|
||||||
|
return nil, "missing_hot_reload_entry"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Validate smem size.
|
||||||
|
if smem.size == 0 then
|
||||||
|
return nil, "zero_smem_size"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Validate stack alignment and range.
|
||||||
|
local stack_top = stack_top_s.value
|
||||||
|
if stack_top % 8 ~= 0 then
|
||||||
|
return nil, "stack_misaligned"
|
||||||
|
end
|
||||||
|
local p = to_physical(stack_top)
|
||||||
|
if p < 0 or p > 0x1fffff then
|
||||||
|
return nil, "stack_out_of_main_ram"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Collect loaded (SHF_ALLOC, non-SHT_NOBITS) sections and check overlap.
|
||||||
|
local loaded = {}
|
||||||
|
local smem_lo = smem.value
|
||||||
|
local smem_hi = smem.value + smem.size
|
||||||
|
for _, s in ipairs(sections) do
|
||||||
|
-- bit 1 (SHF_ALLOC = 0x2) of sh_flags. The modulo-4 trick matches
|
||||||
|
-- the prior implementation; canonicalising on E.SHF_ALLOC would
|
||||||
|
-- gain readability but lose the exact prior behavior.
|
||||||
|
local is_alloc = (s.sh_flags % 4) >= 2
|
||||||
|
if is_alloc and s.sh_type ~= E.SHT_NOBITS and s.sh_size > 0 then
|
||||||
|
loaded[#loaded + 1] = { name = s.name, addr = s.sh_addr, size = s.sh_size }
|
||||||
|
local lo = s.sh_addr
|
||||||
|
local hi = s.sh_addr + s.sh_size
|
||||||
|
if lo < smem_hi and hi > smem_lo then
|
||||||
|
return nil, "section_overlaps_smem"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
target = target,
|
||||||
|
elf_path = path,
|
||||||
|
elf_entry = hdr.e_entry,
|
||||||
|
smem_addr = smem.value,
|
||||||
|
smem_size = smem.size,
|
||||||
|
bss_start = bss_start.value,
|
||||||
|
bss_end = bss_end.value,
|
||||||
|
data_start = data_start.value,
|
||||||
|
data_end = data_end.value,
|
||||||
|
hot_reload_entry = entry_s and entry_s.value or nil,
|
||||||
|
stack_top = stack_top,
|
||||||
|
loaded_sections = loaded,
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
if inner_ok then
|
||||||
|
return inner_result, inner_err
|
||||||
|
end
|
||||||
|
-- pcall captured a thrown error; surface as parse_error with the
|
||||||
|
-- message + traceback so the caller can render it.
|
||||||
|
local tb = debug.traceback(inner_result, 2)
|
||||||
|
local err = {
|
||||||
|
parse_error = true,
|
||||||
|
detail = tostring(inner_result),
|
||||||
|
tb = tb,
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.parse_manifest(file_adapter, target, path, require_entry)
|
||||||
|
if type(E) ~= "table" or type(E.parse_elf32_headers) ~= "function" then
|
||||||
|
stack()
|
||||||
|
end
|
||||||
|
return parse_manifest_impl(file_adapter, target, path, require_entry)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Runtime + dispatch (Task 4)
|
||||||
|
--
|
||||||
|
-- M.new(host) returns a runtime object that owns:
|
||||||
|
-- active -- the most recently primed manifest, or nil
|
||||||
|
-- busy -- boolean guard; only one request runs at a time
|
||||||
|
-- host -- the bound host surface (pause / memory_file / open_file
|
||||||
|
-- / binary_load / invalidate_cache / get_registers)
|
||||||
|
--
|
||||||
|
-- runtime:handle(req) parses the query through M.parse_query, validates
|
||||||
|
-- the mode against a dispatch table, then acquires the busy guard through
|
||||||
|
-- xpcall so any error inside the handler releases the guard. The response
|
||||||
|
-- is always a JSON string built by M.json_response.
|
||||||
|
--
|
||||||
|
-- M.prime_active and M.elf_reload are the two handler bodies Task 4 ships.
|
||||||
|
-- prime_active always parses with require_entry=false (Phase 0 binary
|
||||||
|
-- compatibility). elf_reload always parses with require_entry=true (the
|
||||||
|
-- new binary must expose hot_reload_entry). Both validate the parsed
|
||||||
|
-- manifest; elf_reload runs the five-field ABI gate before declaring
|
||||||
|
-- success. Full host.pause / memory_file / binary_load / invalidate_cache
|
||||||
|
-- / get_registers sequencing is Task 5.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Convert a manifest into the JSON-serializable field subset. loaded_sections
|
||||||
|
-- is excluded because json_response only supports scalars + nil.
|
||||||
|
local function manifest_to_response(m)
|
||||||
|
local fields = {
|
||||||
|
ok = true,
|
||||||
|
target = m.target,
|
||||||
|
elf_path = m.elf_path,
|
||||||
|
elf_entry = m.elf_entry,
|
||||||
|
smem_addr = m.smem_addr,
|
||||||
|
smem_size = m.smem_size,
|
||||||
|
bss_start = m.bss_start,
|
||||||
|
bss_end = m.bss_end,
|
||||||
|
data_start = m.data_start,
|
||||||
|
data_end = m.data_end,
|
||||||
|
stack_top = m.stack_top,
|
||||||
|
}
|
||||||
|
if m.hot_reload_entry then
|
||||||
|
fields.hot_reload_entry = m.hot_reload_entry
|
||||||
|
end
|
||||||
|
return fields
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Open the new ELF through the host and parse its manifest.
|
||||||
|
-- Returns manifest on success; nil + stable error on failure.
|
||||||
|
local function parse_manifest_via_host(host, target, path, require_entry)
|
||||||
|
local adapter = host.open_file(path)
|
||||||
|
if not adapter then
|
||||||
|
return nil, "open_file_failed"
|
||||||
|
end
|
||||||
|
return M.parse_manifest(adapter, target, path, require_entry)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- prime_active: parse with require_entry=false. Accepts Phase 0 binaries
|
||||||
|
-- that lack hot_reload_entry. Stores the manifest in runtime.active.
|
||||||
|
function M.prime_active(runtime, parsed)
|
||||||
|
local manifest, err = parse_manifest_via_host(
|
||||||
|
runtime.host, parsed.target, parsed.path, false)
|
||||||
|
if not manifest then
|
||||||
|
return M.json_response({ ok = false, error = err, restart_required = true })
|
||||||
|
end
|
||||||
|
runtime.active = manifest
|
||||||
|
return M.json_response(manifest_to_response(manifest))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- elf_reload: full host-driven reload sequence.
|
||||||
|
--
|
||||||
|
-- Per conductor/tracks/ps1_pcsx_redux_hot_reload_20260802/spec.md §5 +
|
||||||
|
-- plan.md Task 5 Step 4. The canonical 11-entry success log is:
|
||||||
|
--
|
||||||
|
-- pause, memory_file, state_read, open_new_elf, binary_load,
|
||||||
|
-- state_restore, invalidate_cache, get_registers, write_sp,
|
||||||
|
-- write_ra, write_pc
|
||||||
|
--
|
||||||
|
-- Sequencing:
|
||||||
|
--
|
||||||
|
-- 1. Validate the request (target == active.target, path present).
|
||||||
|
-- 2. Compute the physical address of `active.smem_addr` via
|
||||||
|
-- strip_kseg; reject if outside physical main RAM.
|
||||||
|
-- 3. PARSE PHASE (before pause):
|
||||||
|
-- a. elf_handle = host.open_file(parsed.path)
|
||||||
|
-- b. manifest = M.parse_manifest(elf_handle, ..., require_entry=true)
|
||||||
|
-- c. Run the five-field ABI gate against runtime.active.
|
||||||
|
-- d. On any rejection here, return BEFORE pause — the runtime
|
||||||
|
-- has invoked host.open_file once (logging "open_file") and
|
||||||
|
-- no other host methods.
|
||||||
|
-- 4. Pause + snapshot:
|
||||||
|
-- host.pause()
|
||||||
|
-- mem = host.memory_file()
|
||||||
|
-- saved = mem:readAtToSlice(active.smem_size, smem_phys)
|
||||||
|
-- 5. RELOAD PHASE:
|
||||||
|
-- elf_handle = host.open_new_elf(parsed.path) -- second open
|
||||||
|
-- loaded = host.binary_load(elf_handle, mem)
|
||||||
|
-- if loaded == nil then return binary_load_failed
|
||||||
|
-- 6. Restore state: mem:writeAtMoveSlice(saved, smem_phys)
|
||||||
|
-- 7. host.invalidate_cache()
|
||||||
|
-- 8. Rewrite SP / RA / PC through the FFI register pointer.
|
||||||
|
-- 9. Replace runtime.active last.
|
||||||
|
-- 10. Return the JSON envelope.
|
||||||
|
--
|
||||||
|
-- The two opens are an intentional test-discoverability choice. The
|
||||||
|
-- PARSE phase uses host.open_file (it is an existing Task 4 surface
|
||||||
|
-- also used by prime); the RELOAD phase uses host.open_new_elf (a
|
||||||
|
-- dedicated Task 5 method). In production both methods bind to
|
||||||
|
-- Support.File.open so the runtime cost is identical to a single open
|
||||||
|
-- — the distinction lives in the test log for ordering verification.
|
||||||
|
local function abi_mismatch_response(field, expected, actual)
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "state_abi_mismatch", field = field,
|
||||||
|
expected = expected, actual = actual,
|
||||||
|
restart_required = true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.elf_reload(runtime, parsed)
|
||||||
|
-- 1. Pre-pause request validation. Pure-Lua, no host calls.
|
||||||
|
if not runtime.active then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "not_primed", restart_required = false })
|
||||||
|
end
|
||||||
|
if parsed.target ~= runtime.active.target then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "target_mismatch",
|
||||||
|
expected = runtime.active.target, actual = parsed.target,
|
||||||
|
restart_required = true })
|
||||||
|
end
|
||||||
|
if type(parsed.path) ~= "string" or parsed.path == "" then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "missing_path",
|
||||||
|
restart_required = false })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 2. SMEM range check on `active` (the new ELF has not been
|
||||||
|
-- parsed yet; the ABI gate below enforces it cannot relocate).
|
||||||
|
local smem_phys = strip_kseg(runtime.active.smem_addr)
|
||||||
|
if smem_phys == nil or smem_phys < 0 or smem_phys > 0x1fffff then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "smem_out_of_main_ram",
|
||||||
|
restart_required = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 3. PARSE PHASE — open + parse + ABI gate. On any rejection here,
|
||||||
|
-- only host.open_file has been called. Pause and downstream
|
||||||
|
-- mutations do NOT occur.
|
||||||
|
local elf_handle_for_parse = runtime.host.open_file(parsed.path)
|
||||||
|
if not elf_handle_for_parse then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "open_file_failed",
|
||||||
|
restart_required = true })
|
||||||
|
end
|
||||||
|
local manifest, parse_err = M.parse_manifest(
|
||||||
|
elf_handle_for_parse, parsed.target, parsed.path, true)
|
||||||
|
if not manifest then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = parse_err,
|
||||||
|
restart_required = true })
|
||||||
|
end
|
||||||
|
local active = runtime.active
|
||||||
|
if manifest.smem_addr ~= active.smem_addr then
|
||||||
|
return abi_mismatch_response(
|
||||||
|
"smem_addr", active.smem_addr, manifest.smem_addr)
|
||||||
|
end
|
||||||
|
if manifest.smem_size ~= active.smem_size then
|
||||||
|
return abi_mismatch_response(
|
||||||
|
"smem_size", active.smem_size, manifest.smem_size)
|
||||||
|
end
|
||||||
|
if manifest.bss_start ~= active.bss_start then
|
||||||
|
return abi_mismatch_response(
|
||||||
|
"bss_start", active.bss_start, manifest.bss_start)
|
||||||
|
end
|
||||||
|
if manifest.bss_end ~= active.bss_end then
|
||||||
|
return abi_mismatch_response(
|
||||||
|
"bss_end", active.bss_end, manifest.bss_end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 4. Pause + snapshot smem bytes.
|
||||||
|
runtime.host.pause()
|
||||||
|
local mem = runtime.host.memory_file()
|
||||||
|
local saved = mem:readAtToSlice(active.smem_size, smem_phys)
|
||||||
|
|
||||||
|
-- 5. RELOAD PHASE — second open for binary_load.
|
||||||
|
local elf_handle = runtime.host.open_new_elf(parsed.path)
|
||||||
|
if not elf_handle then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "open_file_failed",
|
||||||
|
restart_required = true })
|
||||||
|
end
|
||||||
|
local loaded = runtime.host.binary_load(elf_handle, mem)
|
||||||
|
if loaded == nil then
|
||||||
|
-- Do NOT restore state; PCSX.Binary.load may have partially
|
||||||
|
-- written RAM. Keep ACTIVE untouched and tell the caller to
|
||||||
|
-- restart the emulator.
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "binary_load_failed",
|
||||||
|
restart_required = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 6. Restore the smem snapshot over the freshly-loaded code.
|
||||||
|
mem:writeAtMoveSlice(saved, smem_phys)
|
||||||
|
|
||||||
|
-- 7. Flush the CPU instruction cache (.text/.rodata changed).
|
||||||
|
runtime.host.invalidate_cache()
|
||||||
|
|
||||||
|
-- 8. Rewrite SP / RA / PC through the FFI register pointer. The
|
||||||
|
-- PC write must happen last; the CPU starts consuming
|
||||||
|
-- instructions at the new PC the moment the emulator resumes.
|
||||||
|
local regs = runtime.host.get_registers()
|
||||||
|
regs.GPR.n.sp = manifest.stack_top
|
||||||
|
regs.GPR.n.ra = 0
|
||||||
|
regs.pc = manifest.hot_reload_entry
|
||||||
|
|
||||||
|
-- 9. Replace ACTIVE last so a failed reload cannot poison the
|
||||||
|
-- next request's gate.
|
||||||
|
runtime.active = manifest
|
||||||
|
|
||||||
|
-- 10. Return the JSON envelope.
|
||||||
|
return M.json_response({
|
||||||
|
ok = true,
|
||||||
|
target = manifest.target,
|
||||||
|
elf_path = manifest.elf_path,
|
||||||
|
elf_entry = manifest.elf_entry,
|
||||||
|
smem_addr = manifest.smem_addr,
|
||||||
|
smem_size = manifest.smem_size,
|
||||||
|
bss_start = manifest.bss_start,
|
||||||
|
bss_end = manifest.bss_end,
|
||||||
|
data_start = manifest.data_start,
|
||||||
|
data_end = manifest.data_end,
|
||||||
|
hot_reload_entry = manifest.hot_reload_entry,
|
||||||
|
stack_top = manifest.stack_top,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- patch_handler: one-word RAM patch through MemoryAsFile.
|
||||||
|
--
|
||||||
|
-- Per spec §7 + plan.md Task 5 Step 5, the order is:
|
||||||
|
-- 1. Parse addr and hex query parameters
|
||||||
|
-- 2. Reject non-hex / missing inputs
|
||||||
|
-- 3. Reject unaligned addresses (addr & 3)
|
||||||
|
-- 4. Normalize through strip_kseg; reject out-of-main-RAM
|
||||||
|
-- (scratchpad 0x1F800000+, BIOS 0x1FC00000+, MMIO, expansion)
|
||||||
|
-- 5. host.pause()
|
||||||
|
-- 6. mem = host.memory_file()
|
||||||
|
-- 7. mem:writeU32At(value, physical_offset)
|
||||||
|
-- 8. host.invalidate_cache()
|
||||||
|
-- 9. Return JSON envelope ok=true with the requested addr and value.
|
||||||
|
local function patch_error(err, restart)
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = err,
|
||||||
|
restart_required = restart or false,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.patch_handler(runtime, parsed)
|
||||||
|
local addr_str = parsed.addr
|
||||||
|
local hex_str = parsed.hex
|
||||||
|
|
||||||
|
-- 1. Presence checks.
|
||||||
|
if type(addr_str) ~= "string" or addr_str == "" then
|
||||||
|
return patch_error("missing_addr", false)
|
||||||
|
end
|
||||||
|
if type(hex_str) ~= "string" or hex_str == "" then
|
||||||
|
return patch_error("missing_value", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 2. Hex parse.
|
||||||
|
local addr = parse_hex_u32(addr_str, "missing_addr", "non_hex_addr")
|
||||||
|
if not addr then
|
||||||
|
return patch_error(
|
||||||
|
addr == false and "missing_addr" or "non_hex_addr", false)
|
||||||
|
end
|
||||||
|
local value = parse_hex_u32(hex_str, "missing_value", "non_hex_value")
|
||||||
|
if not value then
|
||||||
|
return patch_error(
|
||||||
|
value == false and "missing_value" or "non_hex_value", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 3. Alignment (checked on the canonical KSEG/physical addr).
|
||||||
|
if addr % 4 ~= 0 then
|
||||||
|
return patch_error("addr_unaligned", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 4. Range check via strip_kseg (rejects KSEG0 > 0x801fffff, KSEG1 >
|
||||||
|
-- 0xa01fffff, scratchpad, BIOS, MMIO, expansion, etc.).
|
||||||
|
local phys = strip_kseg(addr)
|
||||||
|
if phys == nil then
|
||||||
|
return patch_error("addr_out_of_main_ram", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 5-8. Pause / write / cache invalidate.
|
||||||
|
runtime.host.pause()
|
||||||
|
local mem = runtime.host.memory_file()
|
||||||
|
mem:writeU32At(value, phys)
|
||||||
|
runtime.host.invalidate_cache()
|
||||||
|
|
||||||
|
-- 9. Return the JSON envelope. Echo the requested address and the
|
||||||
|
-- value in normalized hex so log captures stay stable across runs.
|
||||||
|
return M.json_response({
|
||||||
|
ok = true,
|
||||||
|
addr = addr_str,
|
||||||
|
value = "0x" .. string.format("%x", value),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mode dispatch table. Each handler is invoked with (runtime, parsed).
|
||||||
|
-- Tasks 5 adds patch (M.patch_handler); the previous placeholder removed.
|
||||||
|
local DISPATCH = {
|
||||||
|
prime = M.prime_active,
|
||||||
|
elf = M.elf_reload,
|
||||||
|
patch = M.patch_handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Wrap a handler call with the busy guard. The guard is acquired only
|
||||||
|
-- after the mode is validated, so unknown-mode requests do not deadlock
|
||||||
|
-- the runtime. xpcall guarantees the guard is released even if the
|
||||||
|
-- handler throws.
|
||||||
|
local function with_busy_guard(runtime, fn)
|
||||||
|
if runtime.busy then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "reload_busy", restart_required = false })
|
||||||
|
end
|
||||||
|
runtime.busy = true
|
||||||
|
-- Capture both the error text and a full Lua traceback so the user
|
||||||
|
-- can see the actual failing call site instead of a generic
|
||||||
|
-- "internal_error". debug.traceback("", 2) skips this xpcall frame
|
||||||
|
-- and the json_response frame so the trace starts at the handler.
|
||||||
|
local ok, result = xpcall(fn, function(e)
|
||||||
|
return { msg = tostring(e), tb = debug.traceback("", 2) }
|
||||||
|
end)
|
||||||
|
runtime.busy = false
|
||||||
|
if not ok then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "internal_error",
|
||||||
|
detail = result.msg, tb = result.tb,
|
||||||
|
restart_required = true })
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.new(host)
|
||||||
|
if type(host) ~= "table" then
|
||||||
|
error("M.new: host must be a table, got " .. type(host))
|
||||||
|
end
|
||||||
|
local runtime = {
|
||||||
|
active = nil,
|
||||||
|
busy = false,
|
||||||
|
host = host,
|
||||||
|
}
|
||||||
|
function runtime:handle(req)
|
||||||
|
-- 1. Parse the query (M.parse_query returns nil, err on failure).
|
||||||
|
local query = req and req.urlData and req.urlData.query or ""
|
||||||
|
local parsed, parse_err = M.parse_query(query)
|
||||||
|
if not parsed then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = parse_err, restart_required = false })
|
||||||
|
end
|
||||||
|
-- 2. Validate the mode against the dispatch table.
|
||||||
|
local mode = parsed.mode
|
||||||
|
local handler = DISPATCH[mode]
|
||||||
|
if not handler then
|
||||||
|
return M.json_response({
|
||||||
|
ok = false, error = "unknown_mode", restart_required = false })
|
||||||
|
end
|
||||||
|
-- 3. Acquire busy and dispatch via xpcall. Mode validation
|
||||||
|
-- happens BEFORE busy is acquired so unknown-mode requests
|
||||||
|
-- cannot deadlock the runtime.
|
||||||
|
return with_busy_guard(self, function()
|
||||||
|
return handler(self, parsed)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
return runtime
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Install the reload handler on a PCSX-Redux instance.
|
||||||
|
--
|
||||||
|
-- Per plan.md Task 5 Step 5 the adapter binds the canonical host method
|
||||||
|
-- names to the PCSX-Lua FFI surface:
|
||||||
|
--
|
||||||
|
-- pause -> PCSX.pauseEmulator
|
||||||
|
-- memory_file -> PCSX.getMemoryAsFile
|
||||||
|
-- open_file -> Support.File.open(path, "READ")
|
||||||
|
-- binary_load -> PCSX.Binary.load
|
||||||
|
-- invalidate_cache -> PCSX.invalidateCache
|
||||||
|
-- get_registers -> PCSX.getRegisters
|
||||||
|
--
|
||||||
|
-- The returned closure dispatches each request through M.new(host)'s
|
||||||
|
-- runtime:handle so the same prime/elf/patch dispatch machinery is used
|
||||||
|
-- (including the busy guard from Task 4).
|
||||||
|
--
|
||||||
|
-- Missing `PCSX.WebServer.Handlers` is created on demand so callers do
|
||||||
|
-- not have to wire that themselves; if `PCSX` or `Support` is absent a
|
||||||
|
-- single line is printed and the function returns without registering
|
||||||
|
-- a handler.
|
||||||
|
function M.install(pcsx, support)
|
||||||
|
if type(pcsx) ~= "table" then
|
||||||
|
print("[reload] install failed: PCSX is not a table")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if type(support) ~= "table"
|
||||||
|
or type(support.File) ~= "table"
|
||||||
|
or type(support.File.open) ~= "function" then
|
||||||
|
print("[reload] install failed: Support.File.open unavailable")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if type(pcsx.pauseEmulator) ~= "function" then print("[reload] install failed: PCSX.pauseEmulator missing"); return end
|
||||||
|
if type(pcsx.getMemoryAsFile) ~= "function" then print("[reload] install failed: PCSX.getMemoryAsFile missing"); return end
|
||||||
|
if type(pcsx.Binary) ~= "table"
|
||||||
|
or type(pcsx.Binary.load) ~= "function" then print("[reload] install failed: PCSX.Binary.load missing"); return end
|
||||||
|
if type(pcsx.invalidateCache) ~= "function" then print("[reload] install failed: PCSX.invalidateCache missing"); return end
|
||||||
|
if type(pcsx.getRegisters) ~= "function" then print("[reload] install failed: PCSX.getRegisters missing"); return end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- File adapter wrap.
|
||||||
|
--
|
||||||
|
-- The production pcsx-redux Support.File wrapper (see
|
||||||
|
-- toolchain/pcsx-redux/src/lua/fileffi.lua:225-232 + size() around line 203)
|
||||||
|
-- exposes byte-read methods as colon-syntax closures with camelCase names:
|
||||||
|
-- readU8At = function(self, pos) ... end
|
||||||
|
-- readU16At = function(self, pos) ... end
|
||||||
|
-- readU32At = function(self, pos) ... end
|
||||||
|
-- size = function(self) ... end
|
||||||
|
--
|
||||||
|
-- The ELF32 parser (scripts/elf32.lua) uses an explicit-pass shape with
|
||||||
|
-- snake_case names:
|
||||||
|
-- adapter.read_u8_at(off) / adapter.read_u16_at(off) /
|
||||||
|
-- adapter.read_u32_at(off) / adapter.read_size()
|
||||||
|
--
|
||||||
|
-- The install boundary wraps the Support.File return value in a thin
|
||||||
|
-- adapter whose methods forward to the production closures, stripping
|
||||||
|
-- the implicit `self` and re-exporting the names the parser validates.
|
||||||
|
-- Without this wrap, E.validate_adapter returns "bad_file_adapter"
|
||||||
|
-- because adapter.read_u8_at / read_u16_at / read_u32_at / read_size
|
||||||
|
-- are not present on the raw Support.File return.
|
||||||
|
local function wrap_file(f)
|
||||||
|
return {
|
||||||
|
read_u8_at = function(off) return f:readU8At(off) end,
|
||||||
|
read_u16_at = function(off) return f:readU16At(off) end,
|
||||||
|
read_u32_at = function(off) return f:readU32At(off) end,
|
||||||
|
read_size = function() return f:size() end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local host = {
|
||||||
|
pause = function() pcsx.pauseEmulator() end,
|
||||||
|
memory_file = function() return pcsx.getMemoryAsFile() end,
|
||||||
|
open_file = function(path) return wrap_file(support.File.open(path, "READ")) end,
|
||||||
|
-- open_new_elf returns the raw Support.File object because the
|
||||||
|
-- RELOAD phase passes it directly to PCSX.Binary.load which
|
||||||
|
-- expects a real File (with readAt / size), NOT the elf32
|
||||||
|
-- parser adapter (read_u8_at / read_u16_at / read_u32_at /
|
||||||
|
-- read_size). Wrapping it in the adapter here triggers the
|
||||||
|
-- binffi.lua "Expected a File object as first argument" error.
|
||||||
|
open_new_elf = function(path) return support.File.open(path, "READ") end,
|
||||||
|
binary_load = function(elf, mem) return pcsx.Binary.load(elf, mem) end,
|
||||||
|
invalidate_cache = function() pcsx.invalidateCache() end,
|
||||||
|
get_registers = function() return pcsx.getRegisters() end,
|
||||||
|
}
|
||||||
|
local runtime = M.new(host)
|
||||||
|
|
||||||
|
if type(pcsx.WebServer) ~= "table" then pcsx.WebServer = {} end
|
||||||
|
if type(pcsx.WebServer.Handlers) ~= "table" then pcsx.WebServer.Handlers = {} end
|
||||||
|
pcsx.WebServer.Handlers.reload = function(req)
|
||||||
|
return runtime:handle(req)
|
||||||
|
end
|
||||||
|
|
||||||
|
print("[reload] handler installed: reload")
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# scripts/reload.ps1
|
||||||
|
#
|
||||||
|
# PCSX-Redux Lua helper reload client.
|
||||||
|
#
|
||||||
|
# Modes:
|
||||||
|
# elf - Request a full ELF reload. Requires -ElfPath.
|
||||||
|
# patch - Request a single-word RAM patch. Requires -Address and -Word.
|
||||||
|
#
|
||||||
|
# -RequestOnly prints the URI and exits before any network I/O.
|
||||||
|
# -Quiet suppresses the compact-JSON printout on the real path.
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[ValidateSet('elf', 'patch')][string]$Mode = 'elf',
|
||||||
|
[string]$Target = 'hello_camera',
|
||||||
|
[string]$ElfPath = '',
|
||||||
|
[string]$Address = '',
|
||||||
|
[string]$Word = '',
|
||||||
|
[int]$Port = 8080,
|
||||||
|
[switch]$RequestOnly,
|
||||||
|
[switch]$Quiet
|
||||||
|
)
|
||||||
|
|
||||||
|
# mode-specific argument guards
|
||||||
|
switch ($Mode) {
|
||||||
|
'patch' {
|
||||||
|
if ([string]::IsNullOrEmpty($Address) -or [string]::IsNullOrEmpty($Word)) {
|
||||||
|
Write-Error "patch mode requires both -Address and -Word"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'elf' {
|
||||||
|
if ([string]::IsNullOrEmpty($ElfPath)) {
|
||||||
|
Write-Error "elf mode requires -ElfPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build the URL-encoded query string.
|
||||||
|
$queryParts = New-Object System.Collections.Generic.List[string]
|
||||||
|
[void]$queryParts.Add("mode=$([uri]::EscapeDataString($Mode))")
|
||||||
|
[void]$queryParts.Add("target=$([uri]::EscapeDataString($Target))")
|
||||||
|
|
||||||
|
switch ($Mode) {
|
||||||
|
'elf' {
|
||||||
|
[void]$queryParts.Add("path=$([uri]::EscapeDataString($ElfPath))")
|
||||||
|
}
|
||||||
|
'patch' {
|
||||||
|
[void]$queryParts.Add("addr=$([uri]::EscapeDataString($Address))")
|
||||||
|
[void]$queryParts.Add("hex=$([uri]::EscapeDataString($Word))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = "http://localhost:$Port/api/v1/lua/reload?$($queryParts -join '&')"
|
||||||
|
|
||||||
|
# RequestOnly path: emit URI and return before any network I/O.
|
||||||
|
if ($RequestOnly) {
|
||||||
|
Write-Output $uri
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Real request path: POST, decode body if it is a byte array, parse JSON.
|
||||||
|
$response = Invoke-WebRequest -Method Post -Uri $uri
|
||||||
|
|
||||||
|
if ($response.Content -is [byte[]]) {
|
||||||
|
$text = [System.Text.Encoding]::UTF8.GetString([byte[]]$response.Content)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$text = [string]$response.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
$obj = $text | ConvertFrom-Json
|
||||||
|
|
||||||
|
if (-not $Quiet) {
|
||||||
|
$obj | ConvertTo-Json -Compress | Write-Output
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $obj.ok) {
|
||||||
|
$errCode = if ($obj.error) { [string]$obj.error } else { 'unknown' }
|
||||||
|
throw "Reload failed: $errCode"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user