diff --git a/.github/workflows/v8dasm-linux-84.yml b/.github/workflows/v8dasm-linux-84.yml new file mode 100644 index 0000000..55d6304 --- /dev/null +++ b/.github/workflows/v8dasm-linux-84.yml @@ -0,0 +1,51 @@ +# Builds View8's disassembler for V8 8.4.371.22 (NW.js 0.47.2) on Linux. +# Start it by hand: Actions tab -> "v8dasm Linux (V8 8.4)" -> Run workflow. +# Builds two versions side by side (pointer compression on / off). + +name: v8dasm Linux (V8 8.4) + +on: + workflow_dispatch: + +jobs: + build: + name: V8 8.4.371.22 - pointer compression ${{ matrix.pc }} + runs-on: ubuntu-22.04 + timeout-minutes: 340 + strategy: + fail-fast: false + matrix: + pc: [true, false] + env: + V8_VERSION: 8.4.371.22 + PC: ${{ matrix.pc }} + + steps: + - uses: actions/checkout@v4 + + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ + /opt/hostedtoolcache/CodeQL /usr/local/share/boost + df -h / + + - name: Build v8dasm + run: bash linux84/build.sh + + - name: Try View8 on the self-test + continue-on-error: true + run: | + pip install parse + bin=$(ls out/v8dasm-*) + python3 view8.py out/selftest.bin out/selftest.js --path "$bin" + echo "----- decompiled self-test -----" + cat out/selftest.js + + - name: Upload + if: always() + uses: actions/upload-artifact@v4 + with: + name: v8dasm-8.4.371.22-linux-pc-${{ matrix.pc }} + path: out/ + if-no-files-found: ignore + retention-days: 30 diff --git a/configs/v8-versions.json b/configs/v8-versions.json index 0100f33..9d84f3e 100644 --- a/configs/v8-versions.json +++ b/configs/v8-versions.json @@ -1,9 +1,9 @@ { "versions": [ { - "v8_version": "14.2.231.21", - "node_version": "Electron 39.2.7 (Chromium 142)", - "build_args": "v8_enable_pointer_compression=true v8_enable_sandbox=true" + "v8_version": "8.4.371.22", + "node_version": "v14.x (NW.js 0.47.2)", + "build_args": "v8_enable_pointer_compression=false" } ] } diff --git a/linux84/build.sh b/linux84/build.sh new file mode 100644 index 0000000..c45f745 --- /dev/null +++ b/linux84/build.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# build.sh build View8's disassembler (v8dasm) for V8 8.4.371.22 on Ubuntu. + +set -euo pipefail + +V8_VERSION=${V8_VERSION:-8.4.371.22} +PC=${PC:-true} +HERE=$(cd "$(dirname "$0")" && pwd) +OUT=${OUT:-$PWD/out} +WORK=${WORK:-$HOME/v8work} +[ "$PC" = true ] && TAG=on || TAG=off +NAME=v8dasm-$V8_VERSION-pc-$TAG + +step() { echo; echo "=====[ $* ]====="; } + +step "1/8 System tools" +sudo apt-get update -qq +sudo apt-get install -y -qq python2 ninja-build pkg-config curl git xz-utils lsb-release +# python2 for old v8 2020 +sudo ln -sf /usr/bin/python2 /usr/local/bin/python +python --version + +step "2/8 depot_tools (Google's download/build helpers)" +mkdir -p "$WORK" +[ -d "$WORK/depot_tools" ] || + git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git "$WORK/depot_tools" +export PATH="$WORK/depot_tools:$PATH" +gclient --version # first run sets depot_tools up + +step "3/8 V8 $V8_VERSION source (only that version, no history)" +cd "$WORK" +if [ ! -d v8/.git ]; then + git init -q v8 + git -C v8 remote add origin https://chromium.googlesource.com/v8/v8.git +fi +git -C v8 fetch --depth 1 origin "refs/tags/$V8_VERSION:refs/tags/$V8_VERSION" +# detached hea false doesnt work, so is removed. +git -C v8 log -1 --format='checked out %H (%cd)' + +# "managed: False" = gclient leaves our v8 checkout alone and only +# downloads what V8's DEPS file lists (build files, ICU, libc++, ...). +cat > .gclient <<'EOF' +solutions = [{ + "name": "v8", + "url": "https://chromium.googlesource.com/v8/v8.git", + "deps_file": "DEPS", + "managed": False, + "custom_deps": {}, +}] +EOF +gclient sync --nohooks --no-history -j 8 + +step "4/8 Compiler + Linux sysroot (the hooks we actually need)" +cd "$WORK/v8" +python build/linux/sysroot_scripts/install-sysroot.py --arch=x64 +python tools/clang/scripts/update.py +python build/util/lastchange.py -o build/util/LASTCHANGE || { + echo "lastchange failed - writing a placeholder" + echo "LASTCHANGE=0-0" > build/util/LASTCHANGE + date +%s > build/util/LASTCHANGE.committime +} + +step "5/8 Patch V8 so it prints functions" +bash "$HERE/patch-v84.sh" "$WORK/v8" +# replace V8 hello-world sample for V8 build to compile it +cp "$HERE/v8dasm.cpp" samples/hello-world.cc + +step "6/8 Configure (pointer compression = $PC)" +ARGS="is_debug=false +is_component_build=false +target_cpu=\"x64\" +v8_enable_pointer_compression=$PC +v8_enable_disassembler=true +v8_enable_object_print=true +v8_use_external_startup_data=false +icu_use_data_file=false +treat_warnings_as_errors=false +symbol_level=0 +use_goma=false" +ARGS=$(echo $ARGS) +echo "$ARGS" +buildtools/linux64/gn gen out/dasm --args="$ARGS" + +step "7/8 Compile (about an hour)" +/usr/bin/ninja -C out/dasm v8_hello_world +mkdir -p "$OUT" +cp out/dasm/v8_hello_world "$OUT/$NAME" +strip "$OUT/$NAME" || true +ls -lh "$OUT/$NAME" + +step "8/8 Self-test" +cd "$OUT" +"./$NAME" --make-test selftest.bin +"./$NAME" selftest.bin > selftest.txt +head -40 selftest.txt +n=$(grep -c '^Start SharedFunctionInfo' selftest.txt || true) +echo "functions printed: $n" +grep -q 'govid' selftest.txt || { echo "SELF-TEST FAILED: no 'govid' in output"; exit 1; } +grep -q '@ 0 : ' selftest.txt || { echo "SELF-TEST FAILED: no bytecode in output"; exit 1; } +echo "SELF-TEST OK" diff --git a/linux84/patch-v84.sh b/linux84/patch-v84.sh new file mode 100644 index 0000000..a8a8217 --- /dev/null +++ b/linux84/patch-v84.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# patch-v84.sh View8 V8 change, rewrite of V8 8.4.371.22 +# +# usage: bash patch-v84.sh /path/to/v8 +# +# V8 targets 10+; the script below makes similiar changes but instead finds each spot by text and not ln number. + +set -euo pipefail +V8=${1:?give the V8 source folder, e.g. ~/v8work/v8} +cd "$V8" + +rep() { + F="$1" OLD="$2" NEW="$3" perl -e ' + my ($f, $old, $new) = @ENV{qw(F OLD NEW)}; + open my $h, "<", $f or die "cannot read $f\n"; + my $s = do { local $/; <$h> }; close $h; + my ($n, $at) = (0, 0); + while (($at = index($s, $old, $at)) >= 0) { $n++; $at += length $old } + if ($n != 1) { + print STDERR "\nPATCH FAILED in $f: expected this text once, found it $n times:\n$old\n"; + exit 1; + } + substr($s, index($s, $old), length $old) = $new; + open $h, ">", $f or die "cannot write $f\n"; print $h $s; close $h; + ' + echo " ok $1" +} + +echo "== 1. src/diagnostics/objects-printer.cc" +F=src/diagnostics/objects-printer.cc + +rep $F ' os << Brief(GetCode()); + } + PrintSourceCode(os); +' ' os << Brief(GetCode()); + } +' + +rep $F ' feedback_metadata().FeedbackMetadataPrint(os); + } else { + os << ""; + } + os << "\n"; +} + +void JSGlobalProxy::JSGlobalProxyPrint' ' feedback_metadata().FeedbackMetadataPrint(os); + } else { + os << ""; + } + os << "\n"; + os << "\nStart BytecodeArray\n"; + if (HasBytecodeArray()) { + GetBytecodeArray().Disassemble(os); + } else { + os << "\n"; + } + os << "\nEnd BytecodeArray\n"; + os << std::flush; +} + +void JSGlobalProxy::JSGlobalProxyPrint' + +echo "== 2. src/objects/objects.cc" +F=src/objects/objects.cc + +rep $F ' case FIXED_ARRAY_TYPE: + os << ""; + break;' ' case FIXED_ARRAY_TYPE: + os << ""; + os << "\nStart FixedArray\n"; + FixedArray::cast(*this).FixedArrayPrint(os); + os << "\nEnd FixedArray\n"; + break;' + +rep $F ' case OBJECT_BOILERPLATE_DESCRIPTION_TYPE: + os << ""; + break;' ' case OBJECT_BOILERPLATE_DESCRIPTION_TYPE: + os << ""; + os << "\nStart ObjectBoilerplateDescription\n"; + ObjectBoilerplateDescription::cast(*this) + .ObjectBoilerplateDescriptionPrint(os); + os << "\nEnd ObjectBoilerplateDescription\n"; + break;' + +rep $F ' case FIXED_DOUBLE_ARRAY_TYPE: + os << ""; + break;' ' case FIXED_DOUBLE_ARRAY_TYPE: + os << ""; + os << "\nStart FixedDoubleArray\n"; + FixedDoubleArray::cast(*this).FixedDoubleArrayPrint(os); + os << "\nEnd FixedDoubleArray\n"; + break;' + +# Inner functions: print the whole function, not just its name. +rep $F ' os << ""; + } + break; + }' ' os << ""; + } + os << "\nStart SharedFunctionInfo\n"; + shared.SharedFunctionInfoPrint(os); + os << "\nEnd SharedFunctionInfo\n"; + break; + }' + +echo "== 3. src/objects/string.cc" +F=src/objects/string.cc + +rep $F ' int len = length(); + if (len > kMaxShortPrintLength) { + accumulator->Add("", internalized_marker, len); + return; + } +' ' int len = length(); +' + +rep $F ' bool truncated = false; + if (len > kMaxShortPrintLength) { + len = kMaxShortPrintLength; + truncated = true; + } +' ' bool truncated = false; +' + +rep $F ' accumulator->Add("Add("Add("Add(" +' + + +rep $F ' if (FLAG_profile_deserialization) PrintF("[Deserializing failed]\n"); + return MaybeHandle(); + } +' ' if (FLAG_profile_deserialization) PrintF("[Deserializing failed]\n"); + return MaybeHandle(); + } + + std::cout << "\nStart SharedFunctionInfo\n"; + result->SharedFunctionInfoPrint(std::cout); + std::cout << "\nEnd SharedFunctionInfo\n"; + std::cout << std::flush; +' + + +rep $F 'SerializedCodeData::SanityCheckResult SerializedCodeData::SanityCheck( + uint32_t expected_source_hash) const { + if (this->size_ < kHeaderSize) return INVALID_HEADER; +' 'SerializedCodeData::SanityCheckResult SerializedCodeData::SanityCheck( + uint32_t expected_source_hash) const { + if (this->size_ < kHeaderSize) return INVALID_HEADER; + std::cout << std::hex + << "[v8dasm] magic file=0x" << GetMagicNumber() + << " this build=0x" << static_cast(kMagicNumber) << "\n" + << "[v8dasm] version file=0x" << GetHeaderValue(kVersionHashOffset) + << " this build=0x" << Version::Hash() << "\n" + << "[v8dasm] flags file=0x" << GetHeaderValue(kFlagHashOffset) + << " this build=0x" << FlagList::Hash() << "\n" + << std::dec << std::flush; + return CHECK_SUCCESS; // the original checks below are now skipped +' + +echo "== 5. src/snapshot/deserializer.cc" +F=src/snapshot/deserializer.cc +rep $F ' CHECK_EQ(magic_number_, SerializedData::kMagicNumber); +' '' + +echo "All V8 8.4 edits applied." diff --git a/linux84/v8dasm.cpp b/linux84/v8dasm.cpp new file mode 100644 index 0000000..cd9a1c4 --- /dev/null +++ b/linux84/v8dasm.cpp @@ -0,0 +1,113 @@ +// v8dasm.cpp dasm for (NW.js 0.47.2) + +#include +#include +#include +#include +#include +#include +#include + +#include "include/libplatform/libplatform.h" +#include "include/v8.h" + + +static const char kTestJs[] = + "var clips = { '8020-8120': 42.833, start: [1, 2, 3], d: [1.5, 2.25] };\n" + "function govid(name) {\n" + " try { if (clips[name] > 1) return name + '!'; }\n" + " catch (e) { return null; }\n" + " switch (name) { case 'a': return 1; case 'b': return 2; }\n" + " return 'x';\n" + "}\n" + "for (var i = 0; i < 3; i++) govid('a' + i);\n"; + +static int MakeTest(v8::Isolate* isolate, const char* out_path) { + v8::ScriptOrigin origin(v8::String::NewFromUtf8Literal(isolate, "test.js")); + v8::ScriptCompiler::Source source( + v8::String::NewFromUtf8Literal(isolate, kTestJs), origin); + v8::Local script; + if (!v8::ScriptCompiler::CompileUnboundScript( + isolate, &source, v8::ScriptCompiler::kEagerCompile) + .ToLocal(&script)) { + std::fprintf(stderr, "test script did not compile\n"); + return 1; + } + std::unique_ptr cache( + v8::ScriptCompiler::CreateCodeCache(script)); + std::ofstream out(out_path, std::ios::binary); + out.write(reinterpret_cast(cache->data), cache->length); + std::printf("wrote %d bytes to %s\n", cache->length, out_path); + return out ? 0 : 1; +} + +static int Disassemble(v8::Isolate* isolate, const char* in_path) { + std::ifstream in(in_path, std::ios::binary); + if (!in) { + std::fprintf(stderr, "cannot open %s\n", in_path); + return 2; + } + std::vector data((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + if (data.empty()) { + std::fprintf(stderr, "%s is empty\n", in_path); + return 2; + } + + auto* cached = new v8::ScriptCompiler::CachedData( + data.data(), static_cast(data.size())); + v8::ScriptOrigin origin(v8::String::NewFromUtf8Literal(isolate, "app.bin")); + v8::ScriptCompiler::Source source( + v8::String::NewFromUtf8Literal(isolate, "\"dummy\""), origin, cached); + + v8::MaybeLocal script = + v8::ScriptCompiler::CompileUnboundScript( + isolate, &source, v8::ScriptCompiler::kConsumeCodeCache); + (void)script; + std::cout << std::flush; + + if (cached->rejected) { + std::fprintf(stderr, "V8 rejected %s (wrong file, or wrong V8 build)\n", + in_path); + return 1; + } + return 0; +} + +int main(int argc, char* argv[]) { + bool make_test = argc == 3 && std::string(argv[1]) == "--make-test"; + if (argc != 2 && !make_test) { + std::fprintf(stderr, + "usage: %s app.bin > disasm.txt\n" + " %s --make-test test.bin\n", + argv[0], argv[0]); + return 2; + } + + v8::V8::SetFlagsFromString("--no-lazy --no-flush-bytecode"); + v8::V8::InitializeICUDefaultLocation(argv[0]); + v8::V8::InitializeExternalStartupData(argv[0]); + std::unique_ptr platform = v8::platform::NewDefaultPlatform(); + v8::V8::InitializePlatform(platform.get()); + v8::V8::Initialize(); + + v8::Isolate::CreateParams params; + params.array_buffer_allocator = + v8::ArrayBuffer::Allocator::NewDefaultAllocator(); + v8::Isolate* isolate = v8::Isolate::New(params); + + int rc; + { + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope handle_scope(isolate); + v8::Local context = v8::Context::New(isolate); + v8::Context::Scope context_scope(context); + rc = make_test ? MakeTest(isolate, argv[2]) : Disassemble(isolate, argv[1]); + } + + isolate->Dispose(); + v8::V8::Dispose(); + v8::V8::ShutdownPlatform(); + delete params.array_buffer_allocator; + return rc; +}