mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
feat: emulate LSE atomics so the Steam client starts on ARMv8.0 CPUs
This commit is contained in:
1 parent
a7c10e3f51
commit
a0c86abcd3
14 files changed
+1183
-1
No files matched your search
@@ -295,6 +295,9 @@ jobs:
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -o "$D/usr/local/bin/droiddeck-glprobe" tools/venus/glprobe.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-glprobe"
|
||||
|
||||
- name: Build the LSE atomics emulator for ARMv8.0 CPUs
|
||||
run: bash tools/linuxfs/lse-emu/build.sh app/src/main/assets/linuxfs
|
||||
|
||||
- name: Check the preloads
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import java.io.File
|
||||
|
||||
object CpuFeatures {
|
||||
internal fun lacksAtomics(cpuInfo: String): Boolean {
|
||||
val cores = cpuInfo.lineSequence()
|
||||
.filter { it.substringBefore(':').trim() == "Features" }
|
||||
.map { it.substringAfter(':').trim().split(Regex("\\s+")) }
|
||||
.toList()
|
||||
return cores.isNotEmpty() && cores.any { "atomics" !in it }
|
||||
}
|
||||
|
||||
val lacksAtomics: Boolean by lazy {
|
||||
runCatching { lacksAtomics(File("/proc/cpuinfo").readText()) }.getOrDefault(false)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.core.CpuFeatures
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.runtime.BwrapSpawner
|
||||
import com.droiddeck.launcher.runtime.DesktopCatalog
|
||||
@@ -18,6 +19,10 @@ object SessionFiles {
|
||||
private const val NO_PAD_SWITCH = "Download/droiddeck-no-pad"
|
||||
const val DIRECTAUDIO_DIR = "usr/local/lib/directaudio"
|
||||
private const val STAGED_LEDGER = "usr/local/.droiddeck-staged"
|
||||
internal const val LSE_EMU = "usr/local/lib/droiddeck-lse/liblse_emu.so"
|
||||
|
||||
internal fun lsePreload(lacksAtomics: Boolean, staged: Boolean): String =
|
||||
if (lacksAtomics && staged) "/$LSE_EMU\n" else ""
|
||||
val DIRECTAUDIO_SETS = arrayOf("linux-wine11", "linux-wine11-systhread")
|
||||
|
||||
internal val DESKTOP_FILES = arrayOf(
|
||||
@@ -195,6 +200,7 @@ object SessionFiles {
|
||||
"usr/local/lib/droiddeck-pipewire.tzst" to "usr/local/lib/droiddeck-pipewire.tzst",
|
||||
"usr/local/share/licenses/uruntime/LICENSE" to "usr/local/share/licenses/uruntime/LICENSE",
|
||||
"usr/local/lib/droiddeck-dirt/ws2_32.dll" to "usr/local/lib/droiddeck-dirt/ws2_32.dll",
|
||||
LSE_EMU to LSE_EMU,
|
||||
) + mangoapp + xclipse.map { it to it } + maliCompat + msitools.map { it to it } + vr + gpuLoad + fexPreloads).filter { (asset, _) ->
|
||||
val dir = asset.substringBeforeLast('/')
|
||||
runCatching { context.assets.list("linuxfs/$dir")?.contains(asset.substringAfterLast('/')) == true }.getOrDefault(false)
|
||||
@@ -252,7 +258,8 @@ object SessionFiles {
|
||||
}
|
||||
ledger.save()
|
||||
for (old in arrayOf("aarch64-unix", "aarch64-windows", "i386-windows")) File(root, "$DIRECTAUDIO_DIR/lib/wine/$old").deleteRecursively()
|
||||
val preload = StringBuilder("/usr/local/lib/libblsession.so\n")
|
||||
val preload = StringBuilder(lsePreload(CpuFeatures.lacksAtomics, File(root, LSE_EMU).isFile))
|
||||
preload.append("/usr/local/lib/libblsession.so\n")
|
||||
if (!File(Environment.getExternalStorageDirectory(), NO_PAD_SWITCH).exists()) {
|
||||
preload.append("/usr/local/lib/libfakeinput.so\n")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import com.droiddeck.launcher.session.SessionFiles
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class CpuFeaturesTest {
|
||||
@Test fun anArmv80CpuLacksAtomics() {
|
||||
val cpuInfo = """
|
||||
processor : 0
|
||||
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
|
||||
processor : 1
|
||||
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
|
||||
""".trimIndent()
|
||||
assertTrue(CpuFeatures.lacksAtomics(cpuInfo))
|
||||
}
|
||||
|
||||
@Test fun oneCoreWithoutAtomicsIsEnough() {
|
||||
val cpuInfo = """
|
||||
processor : 0
|
||||
Features : fp asimd atomics
|
||||
processor : 4
|
||||
Features : fp asimd
|
||||
""".trimIndent()
|
||||
assertTrue(CpuFeatures.lacksAtomics(cpuInfo))
|
||||
}
|
||||
|
||||
@Test fun anArmv82CpuHasAtomics() {
|
||||
val cpuInfo = """
|
||||
processor : 0
|
||||
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
|
||||
""".trimIndent()
|
||||
assertFalse(CpuFeatures.lacksAtomics(cpuInfo))
|
||||
}
|
||||
|
||||
@Test fun noFeatureLinesMeansNoEmulation() {
|
||||
assertFalse(CpuFeatures.lacksAtomics("processor : 0\n"))
|
||||
assertFalse(CpuFeatures.lacksAtomics(""))
|
||||
}
|
||||
|
||||
@Test fun theEmulatorIsPreloadedFirstOnlyWhenNeededAndStaged() {
|
||||
assertEquals("/usr/local/lib/droiddeck-lse/liblse_emu.so\n", SessionFiles.lsePreload(lacksAtomics = true, staged = true))
|
||||
assertEquals("", SessionFiles.lsePreload(lacksAtomics = true, staged = false))
|
||||
assertEquals("", SessionFiles.lsePreload(lacksAtomics = false, staged = true))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
# Steam client on ARMv8.0 CPUs
|
||||
|
||||
## The crash
|
||||
|
||||
Valve's native arm64 Steam client stopped starting on ARMv8.0 CPUs after the April 15 2026
|
||||
build (client version 1776387948). The process dies with SIGILL right away, the session log
|
||||
shows `"$steam_root/steamrtarm64/steam" ... Illegal instruction` and `steam exited rc=132`,
|
||||
and the screen stays black.
|
||||
|
||||
The faulting instruction is an ARMv8.1 LSE atomic. The GDB trace in
|
||||
[ValveSoftware/steam-for-linux#13288](https://github.com/ValveSoftware/steam-for-linux/issues/13288)
|
||||
stops on `ldaddal x1, x0, [x0]`, with `swpa` and `casal` next to it. Earlier builds were most
|
||||
likely compiled with `-moutline-atomics`, which picks LL/SC or LSE at run time. Newer builds
|
||||
inline LSE. Valve has not replied on that issue, so the cause comes from the reporter's
|
||||
disassembly, not from Valve.
|
||||
|
||||
Affected cores are the ARMv8.0 ones without LSE: Cortex-A35, A53, A57, A72 and A73. A55, A75
|
||||
and later have it. On Android that covers the Snapdragon 4xx/6xx parts built on A53/A73 (for example the
|
||||
Snapdragon 685 with Adreno 610 in
|
||||
[Droid-Deck/DroidDeck#380](https://github.com/Droid-Deck/DroidDeck/issues/380)), plus the
|
||||
Nintendo Switch, Raspberry Pi 4 and older, and the Jetson Nano outside Android.
|
||||
`/proc/cpuinfo` shows it: the `Features` line has no `atomics`.
|
||||
|
||||
LSE is an ARMv8.1 feature. LSE-Emu users report the current client running on a Cortex-A57,
|
||||
so nothing later than LSE seems to be required yet.
|
||||
|
||||
## What other projects do
|
||||
|
||||
- [SildurFX/Switchdeck](https://github.com/SildurFX/Switchdeck) (Switch, L4T Linux) lets the
|
||||
client update once, then overlays `linuxarm64`, `steamrtarm64`, `steamui` and a `steam.cfg`
|
||||
from the April build. It works there because the install script controls the whole system.
|
||||
For us a pinned client is replaced by the self updater on its next start, which is why #380
|
||||
was closed.
|
||||
- [SildurFX/LSE-Emu](https://github.com/SildurFX/LSE-Emu) is a preload library by the same
|
||||
author, written to run the current arm64 client on the Switch. A SIGILL handler performs
|
||||
each LSE instruction with an LL/SC loop. With `LSE_EMU_PATCH=1` the first trap at a site
|
||||
writes an LL/SC stub within branch range and replaces the instruction with a `b` to it, so
|
||||
the site runs natively after that. It covers the full LSE set plus `LDAPR`. The author
|
||||
reports Steam, FEX, Proton games and ARMSX2 running on a Cortex-A57, with ARMSX2 close to
|
||||
native ARMv8.0 speed once patched. GPL-3.0, same as this repo.
|
||||
- [Azkali/proton-arm64-a57](https://github.com/Azkali/proton-arm64-a57) rebuilds Proton 11
|
||||
arm64 (Wine, DXVK, vkd3d-proton, FEX's WoW64 and ARM64EC DLLs) with
|
||||
`-march=armv8-a -mtune=cortex-a57`. Stock Proton 11 arm64 also SIGILLs on ARMv8.0
|
||||
(`wineboot` rc=132), so the client is not the only problem for games.
|
||||
- Some installers fall back to Valve's x86-64 client under FEX on CPUs without LSE. We already
|
||||
have that path (`BL_STEAM_CLIENT_ARCH=x86_64`, `droiddeck-steam-x64`), but it is slower and
|
||||
FEX itself has to be built without LSE.
|
||||
- [FrankNat996/armor](https://github.com/FrankNat996/armor) notes a separate problem: the arm64
|
||||
client's ELF segments are 4K aligned, so it SIGSEGVs at once on 16K page kernels.
|
||||
|
||||
## This change
|
||||
|
||||
The session vendors LSE-Emu (`tools/linuxfs/lse-emu`, upstream commit `8bc9548`). CI and
|
||||
`tools/build_local.sh` build it as `usr/local/lib/droiddeck-lse/liblse_emu.so` with
|
||||
`-march=armv8-a -mno-outline-atomics`. The build fails if the library contains LSE
|
||||
instructions itself.
|
||||
|
||||
When `/proc/cpuinfo` reports any core without `atomics`, `SessionFiles` puts the library first
|
||||
in the rootfs `/etc/ld.so.preload`. `BwrapSpawner` already turns that file into `LD_PRELOAD`
|
||||
for sandboxed processes. On CPUs with LSE nothing changes: the file is staged but not
|
||||
preloaded, and the library would also stay idle on its own because it checks `HWCAP_ATOMICS`.
|
||||
|
||||
`droiddeck-session` sees the library in `ld.so.preload` and exports `LSE_EMU=1` and
|
||||
`LSE_EMU_PATCH=1`, so the Steam client and everything it starts get the emulation. With a
|
||||
session debug folder it also sets `LSE_EMU_LOG=$BL_DEBUG_DIR/lse_emu.log`, and `session.log`
|
||||
gets a line starting with `== cpu: no LSE atomics`.
|
||||
|
||||
The app targets SDK 28, so the app domain still has `execmod` and the in-place patching can
|
||||
make file-backed code pages writable. If the target SDK goes up, `mprotect` on those pages is
|
||||
refused, every site logs `mprotect-failed`, and execution stays on the slower trap path.
|
||||
|
||||
## Tests
|
||||
|
||||
`tools/tests/test_lse_emu.py` builds the library and a program that uses every LSE family
|
||||
(`ldadd` in all four sizes, `ldclr`, `ldeor`, `ldset`, `swp`, `ldsmax`, `ldumin`, `casal`,
|
||||
`caspal`) from four threads, and runs it under `qemu-aarch64 -cpu cortex-a57`:
|
||||
|
||||
- without the library the program dies with SIGILL
|
||||
- trapping alone runs every form and keeps the threaded counters exact
|
||||
- patching runs every form and patches every site, single threaded
|
||||
- without `LSE_EMU=1`, and on `-cpu neoverse-n1`, the library stays out of the way
|
||||
- a SIGILL handler the program installs itself still runs for an unrelated `udf`
|
||||
|
||||
Threaded patching segfaults under qemu-user, writing to the stub arena, which is mapped and
|
||||
writable. qemu write-protects pages it has translated code from, and that races when other
|
||||
threads run code on them. It needs checking on real hardware.
|
||||
|
||||
## Testing on a device
|
||||
|
||||
On an ARMv8.0 phone or handheld (no `atomics` in `/proc/cpuinfo`):
|
||||
|
||||
1. Install the test APK and start a Steam session.
|
||||
2. `session.log` should have `== cpu: no LSE atomics` and no `steam exited rc=132`.
|
||||
3. In the session debug folder, `lse_emu.log` should show `start`, `patched` and `exit` lines.
|
||||
`cut -d' ' -f2 lse_emu.log | sort | uniq -c` summarises it. `unhandled` means an
|
||||
instruction the library does not cover, and `mprotect-failed` or `no-arena` mean sites left
|
||||
on the trap path.
|
||||
4. Compare UI responsiveness against an ARMv8.2 device, and note any `lse_emu: unhandled`
|
||||
lines in `session.log`.
|
||||
|
||||
## Still open
|
||||
|
||||
- Games. Proton 11 arm64 and Valve's FEX build need the same treatment. The preload reaches
|
||||
them when they inherit `LD_PRELOAD`, but pressure-vessel and static binaries can drop it,
|
||||
and FEX installs its own signal handlers with raw syscalls. An ARMv8.0 Proton build like
|
||||
Azkali's is the cleaner fix.
|
||||
- In #380 the session first died with SIGILL in the app's own `wl-compositor` thread, inside
|
||||
`turnip-sdk36/vulkan.ad08XX.so`, which is the A8xx driver build loaded on an Adreno 610.
|
||||
Picking the 6xx/7xx Turnip fixed it, and Auto picks that build once the driver release list
|
||||
is loaded. Nobody has checked yet whether our bundled driver builds avoid LSE. The preload
|
||||
cannot help there because the compositor is a bionic thread in the app process.
|
||||
- JIT code (anonymous memory) is never patched and always traps.
|
||||
- If Valve starts requiring something past LSE, the library prints
|
||||
`lse_emu: unhandled pc=... insn=...` and the client crashes as before.
|
||||
@@ -148,6 +148,7 @@ docker run --rm --platform "${docker_platform}" \
|
||||
external/MaliBridge/scripts/build-client-linux.sh "$d" aarch64-linux-gnu-gcc
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -o "$d/usr/local/bin/droiddeck-glprobe" tools/venus/glprobe.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$d/usr/local/bin/droiddeck-glprobe"
|
||||
bash tools/linuxfs/lse-emu/build.sh "$d"
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$d/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$d/usr/local/bin/droiddeck-clipboard"
|
||||
for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do
|
||||
|
||||
@@ -31,5 +31,9 @@ for sym in vkNegotiateLoaderLayerInterfaceVersion vkGetInstanceProcAddr vkGetDev
|
||||
grep -qx "$sym" <<<"$XCLIPSE" || { echo "::error::the Xclipse layer does not export $sym"; exit 1; }
|
||||
done
|
||||
test -f "$D/usr/local/lib/droiddeck-xclipse/VkLayer_droiddeck_xclipse.json"
|
||||
LSE=$(syms "$D/usr/local/lib/droiddeck-lse/liblse_emu.so")
|
||||
for sym in sigaction signal sigprocmask pthread_sigmask prctl syscall; do
|
||||
grep -qx "$sym" <<<"$LSE" || { echo "::error::liblse_emu.so does not export $sym"; exit 1; }
|
||||
done
|
||||
test -f "$D/usr/local/bin/droiddeck-session"
|
||||
test -f "$D/usr/local/bin/droiddeck-proton-extra"
|
||||
@@ -0,0 +1,15 @@
|
||||
# LSE-Emu
|
||||
|
||||
`lse_emu.c` is an unmodified copy of [SildurFX/LSE-Emu](https://github.com/SildurFX/LSE-Emu)
|
||||
at commit `8bc9548ac6fd24a8d506b266b17ba8d0e71a9b01` (GPL-3.0-or-later).
|
||||
|
||||
It is an `LD_PRELOAD` library that emulates the ARMv8.1 LSE atomic instructions (and `LDAPR`)
|
||||
on ARMv8.0 CPUs. A SIGILL handler performs each trapped instruction with an LL/SC loop. With
|
||||
`LSE_EMU_PATCH=1` the first trap at a site writes an LL/SC stub nearby and replaces the
|
||||
instruction with a branch to it, so later runs do not trap.
|
||||
|
||||
The session stages it as `/usr/local/lib/droiddeck-lse/liblse_emu.so` and only lists it in
|
||||
`/etc/ld.so.preload` when `/proc/cpuinfo` has no `atomics` flag. See
|
||||
`docs/arm-v8.0-steam-client.md` for why.
|
||||
|
||||
To update, copy `lse_emu.c` from a newer upstream commit and change the commit above.
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
D=${1:?asset directory}
|
||||
CC=${CC:-aarch64-linux-gnu-gcc}
|
||||
STRIP=${STRIP:-aarch64-linux-gnu-strip}
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
out="$D/usr/local/lib/droiddeck-lse/liblse_emu.so"
|
||||
mkdir -p "$(dirname "$out")"
|
||||
"$CC" -shared -fPIC -O2 -march=armv8-a -mno-outline-atomics -Wall -Wextra -Wno-unused-parameter \
|
||||
-Wl,-z,now,-z,max-page-size=65536 -o "$out" "$here/lse_emu.c" -ldl
|
||||
"$STRIP" --strip-unneeded "$out"
|
||||
if "${OBJDUMP:-aarch64-linux-gnu-objdump}" -d "$out" | grep -qE $'\t(ld(add|clr|eor|set|smax|smin|umax|umin)|swp|cas)[a-z]*\t'; then
|
||||
echo "::error::liblse_emu.so itself contains LSE instructions"; exit 1
|
||||
fi
|
||||
@@ -0,0 +1,756 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// https://github.com/SildurFX/LSE-Emu
|
||||
// Copyright (C) 2026 SildurFX
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <signal.h>
|
||||
#include <ucontext.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/auxv.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/syscall.h>
|
||||
|
||||
#ifndef HWCAP_ATOMICS
|
||||
#define HWCAP_ATOMICS (1UL << 8)
|
||||
#endif
|
||||
#ifndef SA_RESTORER
|
||||
#define SA_RESTORER 0x04000000
|
||||
#endif
|
||||
#ifndef MAP_FIXED_NOREPLACE
|
||||
#define MAP_FIXED_NOREPLACE 0x100000
|
||||
#endif
|
||||
|
||||
/* ======================= raw syscalls (no PLT in handler) ================= */
|
||||
|
||||
static long sc(long n, long a, long b, long c, long d, long e, long f) {
|
||||
register long x8 asm("x8") = n, x0 asm("x0") = a, x1 asm("x1") = b;
|
||||
register long x2 asm("x2") = c, x3 asm("x3") = d, x4 asm("x4") = e, x5 asm("x5") = f;
|
||||
asm volatile("svc 0" : "+r"(x0)
|
||||
: "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5)
|
||||
: "memory", "cc");
|
||||
return x0;
|
||||
}
|
||||
|
||||
/* ======================= slow path: emulation ============================= */
|
||||
|
||||
#define DEFINE_RMW(NAME, UT, ST) \
|
||||
static UT NAME(void *p, unsigned op, UT v) { \
|
||||
UT *q = (UT *)p; \
|
||||
switch (op) { \
|
||||
case 0: return __atomic_fetch_add(q, v, __ATOMIC_SEQ_CST); \
|
||||
case 1: return __atomic_fetch_and(q, (UT)~v, __ATOMIC_SEQ_CST); \
|
||||
case 2: return __atomic_fetch_xor(q, v, __ATOMIC_SEQ_CST); \
|
||||
case 3: return __atomic_fetch_or (q, v, __ATOMIC_SEQ_CST); \
|
||||
case 8: return __atomic_exchange_n(q, v, __ATOMIC_SEQ_CST); \
|
||||
} \
|
||||
UT old = __atomic_load_n(q, __ATOMIC_RELAXED), nw; \
|
||||
do { \
|
||||
switch (op) { \
|
||||
case 4: nw = ((ST)old > (ST)v) ? old : v; break; \
|
||||
case 5: nw = ((ST)old < (ST)v) ? old : v; break; \
|
||||
case 6: nw = (old > v) ? old : v; break; \
|
||||
default: nw = (old < v) ? old : v; break; \
|
||||
} \
|
||||
} while (!__atomic_compare_exchange_n(q, &old, nw, 1, \
|
||||
__ATOMIC_SEQ_CST, __ATOMIC_RELAXED));\
|
||||
return old; \
|
||||
}
|
||||
DEFINE_RMW(rmw8, uint8_t, int8_t)
|
||||
DEFINE_RMW(rmw16, uint16_t, int16_t)
|
||||
DEFINE_RMW(rmw32, uint32_t, int32_t)
|
||||
DEFINE_RMW(rmw64, uint64_t, int64_t)
|
||||
|
||||
#define DEFINE_CAS(NAME, UT) \
|
||||
static UT NAME(void *p, UT exp, UT des) { \
|
||||
UT e = exp; \
|
||||
__atomic_compare_exchange_n((UT *)p, &e, des, 0, \
|
||||
__ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); \
|
||||
return e; \
|
||||
}
|
||||
DEFINE_CAS(cas8, uint8_t)
|
||||
DEFINE_CAS(cas16, uint16_t)
|
||||
DEFINE_CAS(cas32, uint32_t)
|
||||
DEFINE_CAS(cas64, uint64_t)
|
||||
|
||||
static void casp128(void *p, uint64_t *o0, uint64_t *o1,
|
||||
uint64_t e0, uint64_t e1, uint64_t d0, uint64_t d1) {
|
||||
uint64_t a, b, t, s;
|
||||
asm volatile(
|
||||
"1: ldaxp %0, %1, [%6]\n"
|
||||
" eor %2, %0, %7\n"
|
||||
" eor %3, %1, %8\n"
|
||||
" orr %2, %2, %3\n"
|
||||
" cbnz %2, 2f\n"
|
||||
" stlxp %w2, %9, %10, [%6]\n"
|
||||
" cbnz %w2, 1b\n"
|
||||
"2: clrex\n"
|
||||
: "=&r"(a), "=&r"(b), "=&r"(t), "=&r"(s)
|
||||
: "0"(0), "1"(0), "r"(p), "r"(e0), "r"(e1), "r"(d0), "r"(d1)
|
||||
: "memory", "cc");
|
||||
*o0 = a; *o1 = b;
|
||||
}
|
||||
|
||||
static int emulate(ucontext_t *uc, uint32_t i) {
|
||||
uint64_t *x = (uint64_t *)uc->uc_mcontext.regs;
|
||||
unsigned rt = i & 31, rn = (i >> 5) & 31, rs = (i >> 16) & 31;
|
||||
unsigned size = i >> 30;
|
||||
void *p = (void *)(rn == 31 ? uc->uc_mcontext.sp : x[rn]);
|
||||
#define RD(r) ((r) >= 31 ? 0 : x[r])
|
||||
#define WR(r, v) do { if ((r) < 31) x[r] = (v); } while (0)
|
||||
|
||||
if ((i & 0x3f200c00) == 0x38200000) { // LD<op>, SWP, LDAPR
|
||||
unsigned o3 = (i >> 15) & 1, opc = (i >> 12) & 7, op;
|
||||
uint64_t v, old;
|
||||
if (!o3) op = opc;
|
||||
else if (opc == 0) op = 8;
|
||||
else if (opc == 4 && rs == 31 &&
|
||||
(i & (1u << 23)) && !(i & (1u << 22))) {
|
||||
switch (size) {
|
||||
case 0: v = __atomic_load_n((uint8_t *)p, __ATOMIC_ACQUIRE); break;
|
||||
case 1: v = __atomic_load_n((uint16_t *)p, __ATOMIC_ACQUIRE); break;
|
||||
case 2: v = __atomic_load_n((uint32_t *)p, __ATOMIC_ACQUIRE); break;
|
||||
default: v = __atomic_load_n((uint64_t *)p, __ATOMIC_ACQUIRE); break;
|
||||
}
|
||||
WR(rt, v);
|
||||
return 1;
|
||||
} else return 0;
|
||||
v = RD(rs);
|
||||
switch (size) {
|
||||
case 0: old = rmw8 (p, op, (uint8_t)v); break;
|
||||
case 1: old = rmw16(p, op, (uint16_t)v); break;
|
||||
case 2: old = rmw32(p, op, (uint32_t)v); break;
|
||||
default: old = rmw64(p, op, v); break;
|
||||
}
|
||||
WR(rt, old);
|
||||
return 1;
|
||||
}
|
||||
if ((i & 0x3fa07c00) == 0x08a07c00) { // CAS
|
||||
uint64_t e = RD(rs), d = RD(rt), old;
|
||||
switch (size) {
|
||||
case 0: old = cas8 (p, (uint8_t)e, (uint8_t)d); break;
|
||||
case 1: old = cas16(p, (uint16_t)e, (uint16_t)d); break;
|
||||
case 2: old = cas32(p, (uint32_t)e, (uint32_t)d); break;
|
||||
default: old = cas64(p, e, d); break;
|
||||
}
|
||||
WR(rs, old);
|
||||
return 1;
|
||||
}
|
||||
if ((i & 0xbfa07c00) == 0x08207c00) { // CASP
|
||||
if ((rs & 1) || (rt & 1)) return 0;
|
||||
uint64_t e0 = RD(rs), e1 = RD(rs + 1), d0 = RD(rt), d1 = RD(rt + 1);
|
||||
if (size == 0) {
|
||||
uint64_t e = (uint32_t)e0 | ((uint64_t)(uint32_t)e1 << 32);
|
||||
uint64_t d = (uint32_t)d0 | ((uint64_t)(uint32_t)d1 << 32);
|
||||
uint64_t o = cas64(p, e, d);
|
||||
WR(rs, (uint32_t)o); WR(rs + 1, o >> 32);
|
||||
} else {
|
||||
uint64_t o0, o1;
|
||||
casp128(p, &o0, &o1, e0, e1, d0, d1);
|
||||
WR(rs, o0); WR(rs + 1, o1);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ======================= fast path: stub generator ======================== */
|
||||
|
||||
#define STP2(a,b) (0xA9BE0000u | ((b) << 10) | (31u << 5) | (a)) // stp a,b,[sp,#-32]!
|
||||
#define LDP2(a,b) (0xA8C20000u | ((b) << 10) | (31u << 5) | (a)) // ldp a,b,[sp],#32
|
||||
#define STR16(c) (0xF9000800u | (31u << 5) | (c)) // str c,[sp,#16]
|
||||
#define LDR16(c) (0xF9400800u | (31u << 5) | (c)) // ldr c,[sp,#16]
|
||||
#define MOVX(d,m) (0xAA0003E0u | ((m) << 16) | (d)) // mov xd,xm
|
||||
#define W(x) do { uint32_t _v = (x); w[n++] = _v; } while (0)
|
||||
|
||||
static uint32_t ldx(unsigned sz, unsigned acq, unsigned rt, unsigned rn) {
|
||||
return (sz << 30) | 0x085F7C00u | (acq << 15) | (rn << 5) | rt;
|
||||
}
|
||||
static uint32_t stx(unsigned sz, unsigned rel, unsigned rs, unsigned rt, unsigned rn) {
|
||||
return (sz << 30) | 0x08007C00u | (rs << 16) | (rel << 15) | (rn << 5) | rt;
|
||||
}
|
||||
static uint32_t alu(unsigned op, unsigned d, unsigned n, unsigned m) {
|
||||
static const uint32_t base[4] = { 0x8B000000u, 0x8A200000u, 0xCA000000u, 0xAA000000u };
|
||||
return base[op] | (m << 16) | (n << 5) | d; // add, bic, eor, orr
|
||||
}
|
||||
|
||||
// CASP{A}{L} Xs,Xs+1,Xt,Xt+1,[Xn] (64-bit pair): ldxp/ldaxp + compare + stxp/stlxp. Four scratch registers, no flag changes, 15 words.
|
||||
static int gen_casp(uint32_t *w, uintptr_t stub, uintptr_t ret, unsigned sz,
|
||||
unsigned rs, unsigned rt, unsigned rn, unsigned acq, unsigned rel) {
|
||||
// sz 1: 64-bit pair (LDXP x), sz 0: 32-bit pair (LDXP w, 8-byte aligned)
|
||||
const uint32_t LDXP = sz ? 0xC87F0000u : 0x887F0000u, STXP = sz ? 0xC8200000u : 0x88200000u;
|
||||
const uint32_t EOR = sz ? 0xCA000000u : 0x4A000000u, ORR = sz ? 0xAA000000u : 0x2A000000u;
|
||||
unsigned t[4], k = 0; int n = 0;
|
||||
for (unsigned r = 9; r <= 17 && k < 4; r++)
|
||||
if (r != rs && r != rs + 1 && r != rt && r != rt + 1 && r != rn) t[k++] = r;
|
||||
if (k < 4) return 0;
|
||||
unsigned t0 = t[0], t1 = t[1], t2 = t[2], t3 = t[3];
|
||||
W(STP2(t0, t1));
|
||||
W(0xA9010000u | (t3 << 10) | (31u << 5) | t2); // stp t2,t3,[sp,#16]
|
||||
int loop = n;
|
||||
W(LDXP | (acq << 15) | (t1 << 10) | (rn << 5) | t0); // ldxp/ldaxp t0,t1,[rn]
|
||||
W(EOR | (rs << 16) | (t0 << 5) | t2); // eor t2,t0,rs
|
||||
W(EOR | ((rs + 1) << 16) | (t1 << 5) | t3); // eor t3,t1,rs+1
|
||||
W(ORR | (t3 << 16) | (t2 << 5) | t2); // orr t2,t2,t3
|
||||
W(0xB5000000u | (3u << 5) | t2); // cbnz t2, clrex (+3)
|
||||
W(STXP | (t2 << 16) | (rel << 15) | ((rt + 1) << 10) | (rn << 5) | rt); // stxp/stlxp w(t2),rt,rt+1,[rn]
|
||||
W(0x35000000u | (((unsigned)(loop - n) & 0x7FFFF) << 5) | t2); // cbnz w(t2), loop
|
||||
W(0xD5033F5Fu); // clrex
|
||||
W(MOVX(rs, t0));
|
||||
W(MOVX(rs + 1, t1));
|
||||
W(0xA9410000u | (t3 << 10) | (31u << 5) | t2); // ldp t2,t3,[sp,#16]
|
||||
W(LDP2(t0, t1));
|
||||
{ int64_t off = (int64_t)ret - (int64_t)(stub + 4 * (uintptr_t)n);
|
||||
W(0x14000000u | (((uint32_t)(off >> 2)) & 0x03FFFFFFu)); }
|
||||
return n;
|
||||
}
|
||||
|
||||
// Builds the stub for one instruction. Returns word count, 0 if unsupported.
|
||||
static int gen(uint32_t *w, uintptr_t stub, uintptr_t ret, uint32_t i) {
|
||||
unsigned size = i >> 30, rt = i & 31, rn = (i >> 5) & 31, rs = (i >> 16) & 31;
|
||||
unsigned kind, op = 0, acq, rel; // kind: 0 ldop, 1 swp, 2 cas, 3 casp, 4 min/max
|
||||
int n = 0;
|
||||
|
||||
if ((i & 0x3FFFFC00u) == 0x38BFC000u) { // LDAPR[B|H]: stub = LDAR[B|H] (same operands, stronger ordering) ; b ret
|
||||
W((i & 0xC0000000u) | 0x08DFFC00u | (i & 0x3FFu));
|
||||
{ int64_t off = (int64_t)ret - (int64_t)(stub + 4 * (uintptr_t)n);
|
||||
W(0x14000000u | (((uint32_t)(off >> 2)) & 0x03FFFFFFu)); }
|
||||
return n;
|
||||
}
|
||||
if ((i & 0x3f200c00) == 0x38200000) {
|
||||
unsigned o3 = (i >> 15) & 1, opc = (i >> 12) & 7;
|
||||
if (!o3 && opc <= 3) { kind = 0; op = opc; }
|
||||
else if (!o3) { kind = 4; op = opc; } // LDSMAX LDSMIN LDUMAX LDUMIN (opc 4..7)
|
||||
else if (o3 && opc == 0) kind = 1;
|
||||
else return 0;
|
||||
acq = (i >> 23) & 1; rel = (i >> 22) & 1;
|
||||
} else if ((i & 0x3fa07c00) == 0x08a07c00) {
|
||||
kind = 2; acq = (i >> 22) & 1; rel = (i >> 15) & 1;
|
||||
} else if ((i & 0xbfa07c00) == 0x08207c00) { // CASP, 32-bit (size 0) and 64-bit (size 1) pairs
|
||||
if (size > 1 || (rs & 1) || (rt & 1) || rs >= 30 || rt >= 30) return 0;
|
||||
kind = 3; acq = (i >> 22) & 1; rel = (i >> 15) & 1;
|
||||
} else return 0;
|
||||
if (rn == 31) return 0; // sp-based: leave to trap path
|
||||
if (kind == 3) return gen_casp(w, stub, ret, size, rs, rt, rn, acq, rel);
|
||||
|
||||
unsigned t[3], k = 0; // three scratch regs from x9..x17
|
||||
for (unsigned r = 9; r <= 17 && k < 3; r++)
|
||||
if (r != rs && r != rt && r != rn) t[k++] = r;
|
||||
unsigned t0 = t[0], t1 = t[1], t2 = t[2];
|
||||
|
||||
W(STP2(t0, t1));
|
||||
W(STR16(t2));
|
||||
int loop = n;
|
||||
W(ldx(size, acq, t0, rn)); // t0 = old
|
||||
if (kind == 1) {
|
||||
W(stx(size, rel, t2, rs, rn));
|
||||
W(0x35000000u | (((unsigned)(loop - n) & 0x7FFFF) << 5) | t2);
|
||||
} else if (kind == 4) { // LDSMAX/LDSMIN/LDUMAX/LDUMIN
|
||||
static const unsigned cc[4] = { 0xC /*gt*/, 0xB /*lt*/, 0x8 /*hi*/, 0x3 /*lo*/ };
|
||||
unsigned sgn = !(op & 2), sf = (size == 3), cmpn = t0;
|
||||
if (size < 2 && sgn) { // sign-extend the loaded value: sxtb/sxth wt1,wt0
|
||||
W((size == 1 ? 0x13003C00u : 0x13001C00u) | (t0 << 5) | t1);
|
||||
cmpn = t1;
|
||||
}
|
||||
if (size < 2) // cmp wcmpn, wrs, {u,s}xt{b,h}
|
||||
W(0x6B20001Fu | (rs << 16) | ((sgn ? (size == 1 ? 5u : 4u) : (size == 1 ? 1u : 0u)) << 13) | (cmpn << 5));
|
||||
else // cmp t0, rs
|
||||
W((sf ? 0xEB00001Fu : 0x6B00001Fu) | (rs << 16) | (t0 << 5));
|
||||
W((sf ? 0x9A800000u : 0x1A800000u) | (rs << 16) | (cc[op - 4] << 12) | (t0 << 5) | t1); // csel t1,t0,rs,cond
|
||||
W(stx(size, rel, t2, t1, rn));
|
||||
W(0x35000000u | (((unsigned)(loop - n) & 0x7FFFF) << 5) | t2);
|
||||
} else if (kind == 0) {
|
||||
W(alu(op, t1, t0, rs)); // t1 = new
|
||||
W(stx(size, rel, t2, t1, rn));
|
||||
W(0x35000000u | (((unsigned)(loop - n) & 0x7FFFF) << 5) | t2);
|
||||
} else {
|
||||
if (size == 3) W(0xCA000000u | (rs << 16) | (t0 << 5) | t1);
|
||||
else if (size == 2) W(0x4A000000u | (rs << 16) | (t0 << 5) | t1); // 32-bit eor
|
||||
else {
|
||||
W((size == 1 ? 0x53003C00u : 0x53001C00u) | (rs << 5) | t1); // uxth/uxtb
|
||||
W(0xCA000000u | (t1 << 16) | (t0 << 5) | t1);
|
||||
}
|
||||
W(0xB5000000u | (3u << 5) | t1); // cbnz t1, fail (+3)
|
||||
W(stx(size, rel, t2, rt, rn));
|
||||
W(0x35000000u | (((unsigned)(loop - n) & 0x7FFFF) << 5) | t2);
|
||||
W(0xD5033F5Fu); // fail: clrex
|
||||
}
|
||||
if (kind == 2) { if (rs != 31) W(MOVX(rs, t0)); }
|
||||
else if (rt != 31) W(MOVX(rt, t0));
|
||||
W(LDR16(t2));
|
||||
W(LDP2(t0, t1));
|
||||
{ int64_t off = (int64_t)ret - (int64_t)(stub + 4 * (uintptr_t)n);
|
||||
W(0x14000000u | (((uint32_t)(off >> 2)) & 0x03FFFFFFu)); }
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ---- stub arenas (must sit within +-128 MiB of the patched code) ---- */
|
||||
|
||||
#define ARENA_SZ 0x10000
|
||||
#define SLOT 64
|
||||
#define MAX_ARENAS 256
|
||||
#define REACH 0x7000000 // 112 MiB, safely inside the 128 MiB branch range
|
||||
static struct { uintptr_t base, used; } arena[MAX_ARENAS];
|
||||
static volatile int narena;
|
||||
|
||||
static int near_(uintptr_t b, uintptr_t pc) {
|
||||
intptr_t d = (intptr_t)b - (intptr_t)pc;
|
||||
return d > -(intptr_t)REACH && d < (intptr_t)REACH;
|
||||
}
|
||||
static int in_arena(uintptr_t a) {
|
||||
int n = __atomic_load_n(&narena, __ATOMIC_ACQUIRE); // pairs with the release store in alloc_slot
|
||||
for (int k = 0; k < n; k++)
|
||||
if (a >= arena[k].base && a < arena[k].base + ARENA_SZ) return 1;
|
||||
return 0;
|
||||
}
|
||||
static unsigned long hexv(const char **p);
|
||||
|
||||
//Find a free 64 KiB-aligned hole within REACH of pc by walking /proc/self/maps called with the patch lock held, so the static buffers are safe
|
||||
static uintptr_t find_gap(uintptr_t pc, const uintptr_t *tr, int ntr) {
|
||||
static char buf[4096], line[512];
|
||||
long fd = sc(SYS_openat, -100, (long)"/proc/self/maps", O_RDONLY | O_CLOEXEC, 0, 0, 0);
|
||||
if (fd < 0) return 0;
|
||||
uintptr_t lo = pc > (uintptr_t)REACH + 0x100000 ? pc - REACH : 0x100000;
|
||||
uintptr_t hi = pc + REACH;
|
||||
uintptr_t prev = 0, best = 0, bestd = ~(uintptr_t)0;
|
||||
int ll = 0; long n;
|
||||
while ((n = sc(SYS_read, fd, (long)buf, sizeof buf, 0, 0, 0)) > 0)
|
||||
for (long k = 0; k < n; k++) {
|
||||
char c = buf[k];
|
||||
if (c != '\n') { if (ll < 511) line[ll++] = c; continue; }
|
||||
line[ll] = 0; ll = 0;
|
||||
const char *p = line;
|
||||
uintptr_t s0 = hexv(&p); if (*p != '-') continue; p++;
|
||||
uintptr_t e0 = hexv(&p);
|
||||
uintptr_t g0 = prev > lo ? prev : lo;
|
||||
g0 = (g0 + 0xFFFF) & ~(uintptr_t)0xFFFF;
|
||||
uintptr_t g1 = (s0 < hi ? s0 : hi) & ~(uintptr_t)0xFFFF;
|
||||
if (g1 > g0 && g1 - g0 >= ARENA_SZ) {
|
||||
uintptr_t rlo = g0, rhi = g1 - ARENA_SZ; // valid block start range
|
||||
uintptr_t cand = pc & ~(uintptr_t)0xFFFF;
|
||||
if (cand < rlo) cand = rlo;
|
||||
if (cand > rhi) cand = rhi;
|
||||
// skip blocks a previous mmap refused, walking outwards inside this hole
|
||||
for (int st = 0; st < 16; st++) {
|
||||
uintptr_t pick = 0; int found = 0;
|
||||
for (int w = 0; w < 2 && !found; w++) {
|
||||
if (w == 1 && st == 0) continue;
|
||||
uintptr_t c;
|
||||
if (w == 0) { if (cand + (uintptr_t)st * ARENA_SZ > rhi) continue; c = cand + (uintptr_t)st * ARENA_SZ; }
|
||||
else { if (cand < rlo + (uintptr_t)st * ARENA_SZ) continue; c = cand - (uintptr_t)st * ARENA_SZ; }
|
||||
int is_bad = 0;
|
||||
for (int t = 0; t < ntr; t++) if (tr[t] == c) is_bad = 1;
|
||||
if (!is_bad) { pick = c; found = 1; }
|
||||
}
|
||||
if (found) {
|
||||
uintptr_t d = pick > pc ? pick - pc : pc - pick;
|
||||
if (d < bestd) { bestd = d; best = pick; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e0 > prev) prev = e0;
|
||||
}
|
||||
sc(SYS_close, fd, 0, 0, 0, 0, 0);
|
||||
return best;
|
||||
}
|
||||
|
||||
static uintptr_t alloc_slot(uintptr_t pc) {
|
||||
for (int k = 0; k < narena; k++)
|
||||
if (arena[k].used + SLOT <= ARENA_SZ && near_(arena[k].base, pc)) {
|
||||
uintptr_t s = arena[k].base + arena[k].used;
|
||||
arena[k].used += SLOT;
|
||||
return s;
|
||||
}
|
||||
if (narena >= MAX_ARENAS) return 0;
|
||||
// free hole nearest to the site, found by walking /proc/self/maps
|
||||
uintptr_t tried[16]; int ntried = 0; // blocks mmap refused, don't offer them again
|
||||
for (int attempt = 0; attempt < 12; attempt++) {
|
||||
uintptr_t hint = find_gap(pc, tried, ntried);
|
||||
if (!hint) return 0;
|
||||
long r = sc(SYS_mmap, hint, ARENA_SZ, PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
if ((unsigned long)r > (unsigned long)-4096) { tried[ntried++] = hint; continue; }
|
||||
if ((uintptr_t)r != hint) { //kernel without MAP_FIXED_NOREPLACE moved it
|
||||
sc(SYS_munmap, r, ARENA_SZ, 0, 0, 0, 0);
|
||||
tried[ntried++] = hint; continue;
|
||||
}
|
||||
arena[narena].base = (uintptr_t)r;
|
||||
arena[narena].used = SLOT;
|
||||
int idx = narena;
|
||||
__atomic_store_n(&narena, idx + 1, __ATOMIC_RELEASE); // publish the slot only after base/used are written
|
||||
return arena[idx].base;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- known executable file-backed ranges (parsed from /proc/self/maps) ---- */
|
||||
|
||||
#define MAX_RANGES 512
|
||||
static struct { uintptr_t lo, hi; } rng[MAX_RANGES];
|
||||
static volatile int nrng;
|
||||
static int misses;
|
||||
|
||||
static int in_ranges(uintptr_t pc) {
|
||||
for (int k = 0; k < nrng; k++) if (pc >= rng[k].lo && pc < rng[k].hi) return 1;
|
||||
return 0;
|
||||
}
|
||||
static unsigned long hexv(const char **p) {
|
||||
unsigned long v = 0;
|
||||
for (;; (*p)++) {
|
||||
char c = **p; int d;
|
||||
if (c >= '0' && c <= '9') d = c - '0';
|
||||
else if (c >= 'a' && c <= 'f') d = c - 'a' + 10;
|
||||
else break;
|
||||
v = (v << 4) | d;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
static void parse_line(const char *p) {
|
||||
unsigned long lo = hexv(&p); if (*p != '-') return; p++;
|
||||
unsigned long hi = hexv(&p); if (*p != ' ') return; p++;
|
||||
if (!(p[0] == 'r' && p[1] == '-' && p[2] == 'x' && p[3] == 'p')) return;
|
||||
for (int f = 0; f < 4 && *p; f++) { while (*p && *p != ' ') p++; while (*p == ' ') p++; }
|
||||
if (*p != '/') return;
|
||||
if (nrng < MAX_RANGES) { rng[nrng].lo = lo; rng[nrng].hi = hi; nrng = nrng + 1; }
|
||||
}
|
||||
static void refresh_ranges(void) {
|
||||
static char buf[4096], line[512];
|
||||
long fd = sc(SYS_openat, -100, (long)"/proc/self/maps", O_RDONLY | O_CLOEXEC, 0, 0, 0);
|
||||
if (fd < 0) return;
|
||||
nrng = 0;
|
||||
int ll = 0; long n;
|
||||
while ((n = sc(SYS_read, fd, (long)buf, sizeof buf, 0, 0, 0)) > 0)
|
||||
for (long k = 0; k < n; k++) {
|
||||
char c = buf[k];
|
||||
if (c == '\n') { line[ll] = 0; parse_line(line); ll = 0; }
|
||||
else if (ll < 511) line[ll++] = c;
|
||||
}
|
||||
sc(SYS_close, fd, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* ---- debug log (LSE_EMU_LOG=1) ---- */
|
||||
|
||||
static int add_str(char *b, int n, const char *s);
|
||||
static int add_dec(char *b, int n, unsigned long v);
|
||||
|
||||
static volatile int logon;
|
||||
static char logpath[200] = "/tmp/lse_emu.log";
|
||||
static unsigned long st_traps, st_patched, st_failed;
|
||||
|
||||
static void logwrite(const char *b, int n) { // raw syscalls, O_APPEND: safe from the handler
|
||||
long fd = sc(SYS_openat, -100, (long)logpath, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0666, 0, 0);
|
||||
if (fd < 0) return;
|
||||
sc(SYS_write, fd, (long)b, n, 0, 0, 0);
|
||||
sc(SYS_close, fd, 0, 0, 0, 0, 0);
|
||||
}
|
||||
static int add_hex(char *b, int n, uint64_t v, int digits) {
|
||||
for (int k = (digits - 1) * 4; k >= 0; k -= 4) b[n++] = "0123456789abcdef"[(v >> k) & 15];
|
||||
return n;
|
||||
}
|
||||
// One line per site, patched is logged as it happens, failures are de-duplicated.
|
||||
static void logsite(const char *why, uintptr_t pc, uint32_t insn) {
|
||||
if (!logon) return;
|
||||
static uintptr_t seen[256]; static volatile int ns;
|
||||
if (why[0] != 'p') { // only "patched" is logged every time
|
||||
for (int k = 0; k < ns; k++) if (seen[k] == pc) return;
|
||||
if (ns >= 256) return;
|
||||
seen[ns] = pc; ns = ns + 1;
|
||||
}
|
||||
char b[160]; int n = 0;
|
||||
n = add_str(b, n, "pid="); n = add_dec(b, n, (unsigned long)sc(SYS_getpid, 0, 0, 0, 0, 0, 0));
|
||||
n = add_str(b, n, " "); n = add_str(b, n, why);
|
||||
n = add_str(b, n, " pc=0x"); n = add_hex(b, n, pc, 16);
|
||||
n = add_str(b, n, " insn=0x"); n = add_hex(b, n, insn, 8);
|
||||
b[n++] = '\n';
|
||||
logwrite(b, n);
|
||||
}
|
||||
static void logstats(const char *tag) {
|
||||
if (!logon) return;
|
||||
char b[160]; int n = 0;
|
||||
n = add_str(b, n, "pid="); n = add_dec(b, n, (unsigned long)sc(SYS_getpid, 0, 0, 0, 0, 0, 0));
|
||||
n = add_str(b, n, " "); n = add_str(b, n, tag);
|
||||
n = add_str(b, n, " traps="); n = add_dec(b, n, st_traps);
|
||||
n = add_str(b, n, " patched="); n = add_dec(b, n, st_patched);
|
||||
n = add_str(b, n, " failed="); n = add_dec(b, n, st_failed);
|
||||
n = add_str(b, n, " arenas="); n = add_dec(b, n, (unsigned long)narena);
|
||||
b[n++] = '\n';
|
||||
logwrite(b, n);
|
||||
}
|
||||
|
||||
/* ---- patching ---- */
|
||||
|
||||
static volatile int plock, patch_enabled, stats;
|
||||
static uintptr_t bad[256];
|
||||
|
||||
// Try-lock, a handler that interrupts try_patch on the same thread and traps again must never spin on a lock it already owns. A skipped site is simply retried on its next trap.
|
||||
static int trylock(void) { return !__atomic_exchange_n(&plock, 1, __ATOMIC_ACQUIRE); }
|
||||
static void unlock(void) { __atomic_store_n(&plock, 0, __ATOMIC_RELEASE); }
|
||||
|
||||
// Overwrite one instruction word with a branch (single aligned release store), then flush the icache.
|
||||
static int patch_word(uintptr_t pc, uint32_t v) {
|
||||
uintptr_t page = pc & ~(uintptr_t)4095;
|
||||
if (sc(SYS_mprotect, page, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, 0, 0, 0)) return -1;
|
||||
__atomic_store_n((uint32_t *)pc, v, __ATOMIC_RELEASE);
|
||||
__builtin___clear_cache((char *)pc, (char *)pc + 4);
|
||||
sc(SYS_mprotect, page, 4096, PROT_READ | PROT_EXEC, 0, 0, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void try_patch(uintptr_t pc, uint32_t insn) {
|
||||
uint32_t w[16];
|
||||
if (bad[(pc >> 2) & 255] == pc) return;
|
||||
if (!gen(w, 0, 0, insn)) { logsite("unsupported-form", pc, insn); return; } // not a patchable form
|
||||
|
||||
if (!trylock()) return;
|
||||
if (*(volatile uint32_t *)pc != insn) goto out; // already patched by another thread
|
||||
if (!in_ranges(pc)) {
|
||||
if (misses++ < 16) refresh_ranges();
|
||||
if (!in_ranges(pc)) { logsite("not-file-backed", pc, insn); bad[(pc >> 2) & 255] = pc; goto out; }
|
||||
}
|
||||
uintptr_t slot = alloc_slot(pc);
|
||||
if (!slot) { logsite("no-arena", pc, insn); bad[(pc >> 2) & 255] = pc; st_failed++; goto out; }
|
||||
int n = gen(w, slot, pc + 4, insn);
|
||||
|
||||
for (int k = 0; k < n; k++) ((volatile uint32_t *)slot)[k] = w[k];
|
||||
__builtin___clear_cache((char *)slot, (char *)slot + 4 * n);
|
||||
|
||||
long off = (long)slot - (long)pc;
|
||||
uint32_t b = 0x14000000u | (((uint32_t)(off >> 2)) & 0x03FFFFFFu);
|
||||
if (patch_word(pc, b)) {
|
||||
logsite("mprotect-failed", pc, insn);
|
||||
bad[(pc >> 2) & 255] = pc; st_failed++; goto out;
|
||||
}
|
||||
st_patched++;
|
||||
logsite("patched", pc, insn);
|
||||
out:
|
||||
unlock();
|
||||
}
|
||||
|
||||
/* ======================= libc hooks (unchanged logic) ===================== */
|
||||
|
||||
static int (*real_sigaction)(int, const struct sigaction *, struct sigaction *);
|
||||
static int (*real_sigprocmask)(int, const sigset_t *, sigset_t *);
|
||||
static int (*real_pthread_sigmask)(int, const sigset_t *, sigset_t *);
|
||||
static long (*real_syscall)(long, ...);
|
||||
|
||||
static void resolve(void) {
|
||||
if (!real_sigaction) real_sigaction = dlsym(RTLD_NEXT, "sigaction");
|
||||
if (!real_sigprocmask) real_sigprocmask = dlsym(RTLD_NEXT, "sigprocmask");
|
||||
if (!real_pthread_sigmask) real_pthread_sigmask = dlsym(RTLD_NEXT, "pthread_sigmask");
|
||||
if (!real_syscall) real_syscall = dlsym(RTLD_NEXT, "syscall");
|
||||
}
|
||||
|
||||
static volatile int active, app_valid;
|
||||
static void * volatile app_h;
|
||||
static volatile unsigned long app_f;
|
||||
struct ksa { void *h; unsigned long f; void *restorer; uint64_t mask; };
|
||||
|
||||
int sigaction(int sig, const struct sigaction *act, struct sigaction *old) {
|
||||
resolve();
|
||||
if (!active || sig != SIGILL) return real_sigaction(sig, act, old);
|
||||
if (old) {
|
||||
memset(old, 0, sizeof *old);
|
||||
if (app_valid) { old->sa_sigaction = app_h; old->sa_flags = (int)(app_f & ~(unsigned long)SA_RESTORER); }
|
||||
else old->sa_handler = SIG_DFL;
|
||||
}
|
||||
if (act) { app_h = (void *)act->sa_sigaction; app_f = (unsigned long)act->sa_flags; app_valid = 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
sighandler_t signal(int sig, sighandler_t h) {
|
||||
struct sigaction sa, o;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_handler = h; sa.sa_flags = SA_RESTART;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
if (sigaction(sig, &sa, &o) < 0) return SIG_ERR;
|
||||
return o.sa_handler;
|
||||
}
|
||||
|
||||
int sigprocmask(int how, const sigset_t *set, sigset_t *old) {
|
||||
resolve();
|
||||
if (active && set && how != SIG_UNBLOCK) {
|
||||
sigset_t t = *set; sigdelset(&t, SIGILL);
|
||||
return real_sigprocmask(how, &t, old);
|
||||
}
|
||||
return real_sigprocmask(how, set, old);
|
||||
}
|
||||
int pthread_sigmask(int how, const sigset_t *set, sigset_t *old) {
|
||||
resolve();
|
||||
if (active && set && how != SIG_UNBLOCK) {
|
||||
sigset_t t = *set; sigdelset(&t, SIGILL);
|
||||
return real_pthread_sigmask(how, &t, old);
|
||||
}
|
||||
return real_pthread_sigmask(how, set, old);
|
||||
}
|
||||
|
||||
// a process that turns on seccomp must not be patched afterwards
|
||||
int prctl(int opt, ...) {
|
||||
va_list ap; va_start(ap, opt);
|
||||
unsigned long a1 = va_arg(ap, unsigned long), a2 = va_arg(ap, unsigned long);
|
||||
unsigned long a3 = va_arg(ap, unsigned long), a4 = va_arg(ap, unsigned long);
|
||||
va_end(ap);
|
||||
resolve();
|
||||
if (opt == PR_SET_SECCOMP) patch_enabled = 0;
|
||||
return (int)real_syscall(SYS_prctl, (long)opt, a1, a2, a3, a4);
|
||||
}
|
||||
|
||||
long syscall(long n, ...) {
|
||||
unsigned long a[6];
|
||||
va_list ap; va_start(ap, n);
|
||||
for (int k = 0; k < 6; k++) a[k] = va_arg(ap, unsigned long);
|
||||
va_end(ap);
|
||||
resolve();
|
||||
if (active) {
|
||||
#ifdef SYS_seccomp
|
||||
if (n == SYS_seccomp) patch_enabled = 0;
|
||||
#endif
|
||||
if (n == SYS_prctl && a[0] == PR_SET_SECCOMP) patch_enabled = 0;
|
||||
if (n == SYS_rt_sigaction && a[0] == SIGILL) {
|
||||
struct ksa *act = (struct ksa *)a[1], *old = (struct ksa *)a[2];
|
||||
if (old) {
|
||||
memset(old, 0, sizeof *old);
|
||||
if (app_valid) { old->h = app_h; old->f = app_f; }
|
||||
}
|
||||
if (act) { app_h = act->h; app_f = act->f; app_valid = 1; }
|
||||
return 0;
|
||||
}
|
||||
if (n == SYS_rt_sigprocmask && a[1] && a[3] == 8 && a[0] != SIG_UNBLOCK) {
|
||||
uint64_t m = *(uint64_t *)a[1];
|
||||
m &= ~(1ULL << (SIGILL - 1));
|
||||
return real_syscall(n, a[0], &m, a[2], a[3], a[4], a[5]);
|
||||
}
|
||||
}
|
||||
return real_syscall(n, a[0], a[1], a[2], a[3], a[4], a[5]);
|
||||
}
|
||||
|
||||
/* ======================= handler ========================================== */
|
||||
|
||||
static void put_hex(uint64_t v) {
|
||||
char b[18]; b[0] = '0'; b[1] = 'x';
|
||||
for (int k = 0; k < 16; k++) b[2 + k] = "0123456789abcdef"[(v >> (60 - 4 * k)) & 15];
|
||||
(void)!write(2, b, 18);
|
||||
}
|
||||
|
||||
static void on_sigill(int sig, siginfo_t *si, void *ctx) {
|
||||
ucontext_t *uc = (ucontext_t *)ctx;
|
||||
uintptr_t pc = uc->uc_mcontext.pc;
|
||||
uint32_t insn = __atomic_load_n((uint32_t *)pc, __ATOMIC_ACQUIRE); // pairs with the release store in patch_word
|
||||
|
||||
if ((insn & 0xFC000000u) == 0x14000000u) { // another thread patched it first
|
||||
intptr_t off = ((int32_t)(insn << 6)) >> 4;
|
||||
if (in_arena(pc + off)) return; // just re-execute the new branch
|
||||
}
|
||||
|
||||
if (emulate(uc, insn)) {
|
||||
uc->uc_mcontext.pc = pc + 4;
|
||||
st_traps++;
|
||||
if (patch_enabled) try_patch(pc, insn);
|
||||
else if (logon) logsite("no-patch", pc, insn);
|
||||
if (logon && !(st_traps & 0x3FFF)) logstats("stats"); // every 16384 traps
|
||||
return;
|
||||
}
|
||||
|
||||
logsite("unhandled", pc, insn);
|
||||
(void)!write(2, "lse_emu: unhandled pc=", 22);
|
||||
put_hex(pc);
|
||||
(void)!write(2, " insn=", 6);
|
||||
put_hex(insn);
|
||||
(void)!write(2, "\n", 1);
|
||||
|
||||
void *h = app_h;
|
||||
if (app_valid && h != (void *)SIG_DFL && h != (void *)SIG_IGN) {
|
||||
if (app_f & SA_SIGINFO) ((void (*)(int, siginfo_t *, void *))h)(sig, si, ctx);
|
||||
else ((void (*)(int))h)(sig);
|
||||
return;
|
||||
}
|
||||
struct sigaction dfl; memset(&dfl, 0, sizeof dfl);
|
||||
dfl.sa_handler = SIG_DFL;
|
||||
real_sigaction(SIGILL, &dfl, NULL);
|
||||
}
|
||||
|
||||
/* ======================= init / fini ====================================== */
|
||||
|
||||
static int add_str(char *b, int n, const char *s) { while (*s) b[n++] = *s++; return n; }
|
||||
static int add_dec(char *b, int n, unsigned long v) {
|
||||
char t[21]; int k = 0;
|
||||
if (!v) t[k++] = '0';
|
||||
while (v) { t[k++] = '0' + v % 10; v /= 10; }
|
||||
while (k) b[n++] = t[--k];
|
||||
return n;
|
||||
}
|
||||
|
||||
static int skipped(void) { // LSE_EMU_SKIP=name:name
|
||||
const char *s = getenv("LSE_EMU_SKIP");
|
||||
if (!s || !*s) return 0;
|
||||
char exe[512]; ssize_t r = readlink("/proc/self/exe", exe, sizeof exe - 1);
|
||||
if (r <= 0) return 0;
|
||||
exe[r] = 0;
|
||||
char tok[128];
|
||||
while (*s) {
|
||||
int k = 0;
|
||||
while (*s && *s != ':' && k < 127) tok[k++] = *s++;
|
||||
tok[k] = 0;
|
||||
if (*s == ':') s++;
|
||||
if (k && strstr(exe, tok)) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void init(void) {
|
||||
resolve();
|
||||
if (getauxval(AT_HWCAP) & HWCAP_ATOMICS) return; // CPU has real LSE
|
||||
|
||||
// Exit silently if LSE_EMU is missing or not set to "1"
|
||||
const char *emu_env = getenv("LSE_EMU");
|
||||
if (!emu_env || emu_env[0] != '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
struct sigaction sa; memset(&sa, 0, sizeof sa);
|
||||
sa.sa_sigaction = on_sigill;
|
||||
sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
real_sigaction(SIGILL, &sa, NULL);
|
||||
active = 1;
|
||||
|
||||
const char *p = getenv("LSE_EMU_PATCH");
|
||||
stats = getenv("LSE_EMU_STATS") != NULL;
|
||||
if (p && p[0] == '1' && !skipped()) {
|
||||
refresh_ranges();
|
||||
patch_enabled = 1;
|
||||
}
|
||||
|
||||
const char *lg = getenv("LSE_EMU_LOG"); // =1 -> /tmp/lse_emu.log, =/path -> that file
|
||||
if (lg && lg[0] && lg[0] != '0') {
|
||||
if (lg[0] == '/') { size_t l = strlen(lg); if (l < sizeof logpath) memcpy(logpath, lg, l + 1); }
|
||||
logon = 1;
|
||||
char b[700], exe[480]; int n = 0;
|
||||
ssize_t r = readlink("/proc/self/exe", exe, sizeof exe - 1);
|
||||
exe[r > 0 ? r : 0] = 0;
|
||||
n = add_str(b, n, "pid="); n = add_dec(b, n, (unsigned long)getpid());
|
||||
n = add_str(b, n, " start patch="); n = add_dec(b, n, (unsigned long)patch_enabled);
|
||||
n = add_str(b, n, " exe="); n = add_str(b, n, exe);
|
||||
b[n++] = '\n';
|
||||
logwrite(b, n);
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((destructor)) static void fini(void) {
|
||||
if (!active) return;
|
||||
logstats("exit");
|
||||
if (!stats) return;
|
||||
char b[160]; int n = 0;
|
||||
n = add_str(b, n, "lse_emu: pid="); n = add_dec(b, n, (unsigned long)getpid());
|
||||
n = add_str(b, n, " traps="); n = add_dec(b, n, st_traps);
|
||||
n = add_str(b, n, " patched="); n = add_dec(b, n, st_patched);
|
||||
n = add_str(b, n, " failed="); n = add_dec(b, n, st_failed);
|
||||
b[n++] = '\n';
|
||||
(void)!write(2, b, n);
|
||||
}
|
||||
@@ -11,6 +11,10 @@ if [ "${BL_RAISE_NOFILE:-0}" = 1 ] && [ -z "${BL_INSIDE:-}" ]; then
|
||||
unset bl_nofile_was
|
||||
fi
|
||||
export HOME=${HOME:-/root}
|
||||
if grep -qs '/liblse_emu\.so$' /etc/ld.so.preload; then
|
||||
export LSE_EMU=1 LSE_EMU_PATCH=1
|
||||
[ -n "${BL_DEBUG_DIR:-}" ] && export LSE_EMU_LOG=${LSE_EMU_LOG:-$BL_DEBUG_DIR/lse_emu.log}
|
||||
fi
|
||||
mode=${1:-desktop}
|
||||
shift || true
|
||||
step() { echo "== STEP $(date -u +%H:%M:%S.%3N) $*"; }
|
||||
@@ -30,6 +34,7 @@ if [ -n "${BL_LOG:-}" ] && [ -z "${BL_INSIDE:-}" ]; then
|
||||
echo "== uid=$(id -u) size=${BL_WIDTH:-?}x${BL_HEIGHT:-?} icd=${VK_ICD_FILENAMES:-none}"
|
||||
echo "== gamescope: $(gamescope --version 2>&1 | head -1) at $(command -v gamescope)"
|
||||
echo "== rootfs: $(cat /.version 2>/dev/null || echo unversioned)"
|
||||
[ "${LSE_EMU:-}" = 1 ] && echo "== cpu: no LSE atomics (ARMv8.0); liblse_emu.so emulates them (log: ${LSE_EMU_LOG:-off})"
|
||||
fi
|
||||
|
||||
start_diagnostic_inventory() {
|
||||
|
||||
@@ -89,6 +89,7 @@ def required():
|
||||
['msiinfo', 'cabextract', '7z', '7z.so', 'libmsi-1.0.so.0', 'libgsf-1.so.114', 'libgcab-1.0.so.0']]
|
||||
paths += ['assets/linuxfs/usr/local/lib/droiddeck-xclipse/' + name for name in
|
||||
['libVkLayer_droiddeck_xclipse.so', 'VkLayer_droiddeck_xclipse.json']]
|
||||
paths += ['assets/linuxfs/usr/local/lib/droiddeck-lse/liblse_emu.so']
|
||||
paths += ['assets/linuxfs/usr/local/lib/droiddeck-vr/' + name for name in
|
||||
['droiddeck/bin/linuxarm64/driver_droiddeck.so', 'droiddeck/bin/linuxarm64/xr-display-mode',
|
||||
'droiddeck/driver.vrdrivermanifest',
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ucontext.h>
|
||||
|
||||
#define MAX_THREADS 8
|
||||
#define ROUNDS 20000
|
||||
|
||||
static uint64_t counter64;
|
||||
static uint32_t counter32;
|
||||
static uint16_t counter16;
|
||||
static uint8_t counter8;
|
||||
static uint64_t spin;
|
||||
|
||||
static void check(int ok, const char *what) {
|
||||
if (!ok) {
|
||||
printf("FAIL %s\n", what);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
static uint64_t ldaddal64(uint64_t *p, uint64_t v) {
|
||||
uint64_t old;
|
||||
__asm__ volatile("ldaddal %x1, %x0, [%2]" : "=r"(old) : "r"(v), "r"(p) : "memory");
|
||||
return old;
|
||||
}
|
||||
|
||||
static uint32_t ldadd32(uint32_t *p, uint32_t v) {
|
||||
uint32_t old;
|
||||
__asm__ volatile("ldadd %w1, %w0, [%2]" : "=r"(old) : "r"(v), "r"(p) : "memory");
|
||||
return old;
|
||||
}
|
||||
|
||||
static uint16_t ldaddlh(uint16_t *p, uint16_t v) {
|
||||
uint16_t old;
|
||||
__asm__ volatile("ldaddlh %w1, %w0, [%2]" : "=r"(old) : "r"(v), "r"(p) : "memory");
|
||||
return old;
|
||||
}
|
||||
|
||||
static uint8_t ldaddab(uint8_t *p, uint8_t v) {
|
||||
uint8_t old;
|
||||
__asm__ volatile("ldaddab %w1, %w0, [%2]" : "=r"(old) : "r"(v), "r"(p) : "memory");
|
||||
return old;
|
||||
}
|
||||
|
||||
static uint64_t casal64(uint64_t *p, uint64_t expected, uint64_t desired) {
|
||||
__asm__ volatile("casal %x0, %x2, [%1]" : "+r"(expected) : "r"(p), "r"(desired) : "memory");
|
||||
return expected;
|
||||
}
|
||||
|
||||
static void *worker(void *arg) {
|
||||
(void)arg;
|
||||
for (int i = 0; i < ROUNDS; i++) {
|
||||
ldaddal64(&counter64, 1);
|
||||
ldadd32(&counter32, 1);
|
||||
ldaddlh(&counter16, 1);
|
||||
ldaddab(&counter8, 1);
|
||||
uint64_t seen = __atomic_load_n(&spin, __ATOMIC_RELAXED);
|
||||
while (casal64(&spin, seen, seen + 1) != seen) seen = __atomic_load_n(&spin, __ATOMIC_RELAXED);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static volatile sig_atomic_t app_handler_ran;
|
||||
|
||||
static void app_sigill(int sig, siginfo_t *info, void *context) {
|
||||
(void)sig;
|
||||
(void)info;
|
||||
app_handler_ran = 1;
|
||||
((ucontext_t *)context)->uc_mcontext.pc += 4;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int threads_wanted = argc > 1 ? atoi(argv[1]) : 4;
|
||||
check(threads_wanted >= 1 && threads_wanted <= MAX_THREADS, "thread count");
|
||||
uint64_t v = 0xF0;
|
||||
uint64_t old;
|
||||
__asm__ volatile("ldclral %x1, %x0, [%2]" : "=r"(old) : "r"((uint64_t)0x30), "r"(&v) : "memory");
|
||||
check(old == 0xF0 && v == 0xC0, "ldclral");
|
||||
__asm__ volatile("ldeora %x1, %x0, [%2]" : "=r"(old) : "r"((uint64_t)0xFF), "r"(&v) : "memory");
|
||||
check(old == 0xC0 && v == 0x3F, "ldeora");
|
||||
__asm__ volatile("ldsetl %x1, %x0, [%2]" : "=r"(old) : "r"((uint64_t)0x100), "r"(&v) : "memory");
|
||||
check(old == 0x3F && v == 0x13F, "ldsetl");
|
||||
__asm__ volatile("swpal %x1, %x0, [%2]" : "=r"(old) : "r"((uint64_t)7), "r"(&v) : "memory");
|
||||
check(old == 0x13F && v == 7, "swpal");
|
||||
|
||||
int64_t s = -5;
|
||||
int64_t sold;
|
||||
__asm__ volatile("ldsmax %x1, %x0, [%2]" : "=r"(sold) : "r"((int64_t)-9), "r"(&s) : "memory");
|
||||
check(sold == -5 && s == -5, "ldsmax keeps the larger value");
|
||||
uint32_t u = 10;
|
||||
uint32_t uold;
|
||||
__asm__ volatile("ldumin %w1, %w0, [%2]" : "=r"(uold) : "r"((uint32_t)3), "r"(&u) : "memory");
|
||||
check(uold == 10 && u == 3, "ldumin");
|
||||
|
||||
uint64_t pair[2] __attribute__((aligned(16))) = {1, 2};
|
||||
register uint64_t e0 __asm__("x4") = 1, e1 __asm__("x5") = 2, d0 __asm__("x6") = 3, d1 __asm__("x7") = 4;
|
||||
__asm__ volatile("caspal x4, x5, x6, x7, [%4]" : "+r"(e0), "+r"(e1) : "r"(d0), "r"(d1), "r"(pair) : "memory");
|
||||
check(e0 == 1 && e1 == 2 && pair[0] == 3 && pair[1] == 4, "caspal");
|
||||
|
||||
uint64_t missed = casal64(&v, 99, 1);
|
||||
check(missed == 7 && v == 7, "casal leaves memory alone on a mismatch");
|
||||
|
||||
pthread_t threads[MAX_THREADS];
|
||||
for (int i = 0; i < threads_wanted; i++) pthread_create(&threads[i], NULL, worker, NULL);
|
||||
for (int i = 0; i < threads_wanted; i++) pthread_join(threads[i], NULL);
|
||||
uint64_t total = (uint64_t)threads_wanted * ROUNDS;
|
||||
check(counter64 == total, "64-bit counter");
|
||||
check(counter32 == (uint32_t)total, "32-bit counter");
|
||||
check(counter16 == (uint16_t)total, "16-bit counter");
|
||||
check(counter8 == (uint8_t)total, "8-bit counter");
|
||||
check(spin == total, "casal loop");
|
||||
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_sigaction = app_sigill;
|
||||
sa.sa_flags = SA_SIGINFO;
|
||||
sigaction(SIGILL, &sa, NULL);
|
||||
__asm__ volatile("udf #0");
|
||||
check(app_handler_ran, "the program's own SIGILL handler still runs");
|
||||
|
||||
printf("lse ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
TOOLS = Path(__file__).resolve().parents[1]
|
||||
SYSROOT = Path("/usr/aarch64-linux-gnu")
|
||||
|
||||
|
||||
def toolchain():
|
||||
if not (shutil.which("aarch64-linux-gnu-gcc") and shutil.which("qemu-aarch64") and SYSROOT.is_dir()):
|
||||
raise unittest.SkipTest("needs aarch64-linux-gnu-gcc, libc6-dev-arm64-cross and qemu-user")
|
||||
|
||||
|
||||
class LseEmuTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
toolchain()
|
||||
cls.tmp = tempfile.TemporaryDirectory(prefix="droiddeck-lse-")
|
||||
out = Path(cls.tmp.name)
|
||||
subprocess.run(["bash", str(TOOLS / "linuxfs/lse-emu/build.sh"), str(out)], check=True)
|
||||
cls.library = out / "usr/local/lib/droiddeck-lse/liblse_emu.so"
|
||||
cls.program = out / "lse-test"
|
||||
subprocess.run(["aarch64-linux-gnu-gcc", "-O1", "-march=armv8.1-a", "-Wall", "-Wextra", "-Werror", "-pthread",
|
||||
str(Path(__file__).with_name("lse_emu_test.c")), "-o", str(cls.program)], check=True)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.tmp.cleanup()
|
||||
|
||||
def run_on(self, cpu, *env, threads=4):
|
||||
command = ["qemu-aarch64", "-cpu", cpu, "-L", str(SYSROOT)]
|
||||
for item in env:
|
||||
command += ["-E", item]
|
||||
return subprocess.run(command + [str(self.program), str(threads)], capture_output=True, text=True, timeout=120)
|
||||
|
||||
def test_an_armv8_0_cpu_faults_without_the_emulator(self):
|
||||
result = self.run_on("cortex-a57")
|
||||
self.assertEqual(result.returncode, -4, result.stdout + result.stderr)
|
||||
|
||||
def test_trapping_alone_runs_every_form_on_an_armv8_0_cpu(self):
|
||||
result = self.run_on("cortex-a57", f"LD_PRELOAD={self.library}", "LSE_EMU=1", "LSE_EMU_STATS=1")
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("lse ok", result.stdout)
|
||||
self.assertRegex(result.stderr, r"traps=[1-9]\d* patched=0 ")
|
||||
|
||||
def test_patching_runs_every_form_on_an_armv8_0_cpu(self):
|
||||
# qemu-user write-protects translated code pages and races when other threads run them while the
|
||||
# stub arena is written, so threaded patching is only meaningful on hardware.
|
||||
result = self.run_on("cortex-a57", f"LD_PRELOAD={self.library}", "LSE_EMU=1", "LSE_EMU_PATCH=1",
|
||||
"LSE_EMU_STATS=1", threads=1)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("lse ok", result.stdout)
|
||||
self.assertRegex(result.stderr, r"patched=[1-9]\d* failed=0")
|
||||
|
||||
def test_it_stays_off_without_the_switch(self):
|
||||
result = self.run_on("cortex-a57", f"LD_PRELOAD={self.library}")
|
||||
self.assertEqual(result.returncode, -4, result.stdout + result.stderr)
|
||||
|
||||
def test_it_stays_off_on_a_cpu_with_lse(self):
|
||||
result = self.run_on("neoverse-n1", f"LD_PRELOAD={self.library}", "LSE_EMU=1", "LSE_EMU_STATS=1")
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertNotIn("traps=", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user