fix(relinker): leave zero-filled PT_LOAD memory out of Windows files

WindowsPeWriter stored every PE section with SizeOfRawData equal to its
virtual size, so the zero-filled tail of a PT_LOAD segment (p_memsz above
p_filesz, the .bss) was written to the executable byte for byte. A segment
with 256 MiB of .bss produced a 269 MB .exe, while the Linux output of the
same image is under 5 KB.

Each section is now stored up to its last non-zero byte, aligned to the file
alignment, and VirtualSize keeps the full size. The PE loader zero-fills the
part of a section beyond SizeOfRawData (Microsoft PE format, section table:
SizeOfRawData). A section with no non-zero byte gets no raw data and a zero
PointerToRawData.

windows_bss relinks an image with 64 MiB of .bss, checks the section table
and the mapped image, and on Windows runs the executable, which reads the
last word of the .bss.
This commit is contained in:
Yassine authored and Dean Galvin committed 2026-10-10 21:23:19 +13:00
1 parent f6af6d11d3
commit 6158444f71
3 files changed
+81 -4

No files matched your search

+2
View File
@@ -253,6 +253,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_working_directory.py $<TARGET_FILE:relinker>)
add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $<TARGET_FILE:relinker>)
add_test(NAME windows_bss COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_bss.py $<TARGET_FILE:relinker>)
add_test(NAME windows_icon COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_icon.py $<TARGET_FILE:relinker>)
add_test(NAME guest_intel_trampolines COMMAND ${Python3_EXECUTABLE}
@@ -44,8 +44,9 @@ std::vector<std::uint8_t> WindowsPeWriter::Write(const std::vector<PeSection>& s
const auto& section = sections[index];
if (section.Name.empty() || section.Name.size() > 8 || section.Data.empty() || section.Rva != endRva)
throw Domain::RelinkerException("Invalid PE section layout: " + section.Name, section.Rva);
const auto rawSize = Io::AlignUp(CheckedRva(section.Data.size()), FileAlignment);
const auto rawOffset = CheckedRva(result.size());
const auto initialized = static_cast<std::size_t>(std::find_if(section.Data.rbegin(), section.Data.rend(), [](const std::uint8_t value) { return value != 0; }).base() - section.Data.begin());
const auto rawSize = Io::AlignUp(CheckedRva(initialized), FileAlignment);
const auto rawOffset = rawSize == 0 ? 0u : CheckedRva(result.size());
const auto header = sectionTable + index * 40;
std::copy(section.Name.begin(), section.Name.end(), result.begin() + static_cast<std::ptrdiff_t>(header));
Io::WriteU32(result, header + 8, CheckedRva(section.Data.size()));
@@ -61,8 +62,10 @@ std::vector<std::uint8_t> WindowsPeWriter::Write(const std::vector<PeSection>& s
} else {
dataSize = CheckedRva(static_cast<std::uint64_t>(dataSize) + rawSize);
}
result.insert(result.end(), section.Data.begin(), section.Data.end());
result.resize(static_cast<std::size_t>(rawOffset) + rawSize);
if (rawSize != 0) {
result.insert(result.end(), section.Data.begin(), section.Data.begin() + static_cast<std::ptrdiff_t>(initialized));
result.resize(static_cast<std::size_t>(rawOffset) + rawSize);
}
endRva = AlignRva(section.Rva + static_cast<std::uint64_t>(section.Data.size()));
}
if (!foundEntry)
@@ -0,0 +1,72 @@
"""Leave the zero-filled tail of PT_LOAD memory out of the Windows file and let the loader zero-fill it."""
import os
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from test_optional_plt import fixture
BSS = 64 << 20
FILE_ALIGNMENT = 0x200
SECTION_ALIGNMENT = 0x1000
LOAD_RVA = 0x10000
def sections(pe):
header = struct.unpack_from("<I", pe, 0x3c)[0]
count = struct.unpack_from("<H", pe, header + 6)[0]
optional = header + 24
table = optional + struct.unpack_from("<H", pe, header + 20)[0]
result = []
for index in range(count):
offset = table + index * 40
name = pe[offset:offset + 8].rstrip(b"\0").decode()
virtual_size, rva, raw_size, raw = struct.unpack_from("<IIII", pe, offset + 8)
result.append((name, virtual_size, rva, raw_size, raw))
return result, struct.unpack_from("<I", pe, optional + 56)[0]
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-windows-bss-") as directory:
work = Path(directory)
image = fixture()
memory_size = struct.unpack_from("<Q", image, 64 + 40)[0] + BSS
struct.pack_into("<Q", image, 64 + 40, memory_size)
last = memory_size - 4
image[0x210:0x21a] = b"\x8b\x05" + struct.pack("<i", last - 0x216) + b"\x83\xc0\x2a\xc3"
source = work / "input.elf"
source.write_bytes(image)
output = work / "output.exe"
result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)],
capture_output=True, text=True, timeout=20)
assert result.returncode == 0, (result.stdout, result.stderr)
pe = output.read_bytes()
assert len(pe) < BSS // 16, f"{len(pe)} byte executable for {BSS} bytes of zero-filled memory"
table, image_size = sections(pe)
first = next(section for section in table if section[2] == LOAD_RVA)
name, virtual_size, rva, raw_size, raw = first
assert virtual_size >= memory_size and rva + virtual_size <= image_size, first
assert 0 < raw_size < SECTION_ALIGNMENT * 2, first
for name, virtual_size, rva, raw_size, raw in table:
assert raw_size % FILE_ALIGNMENT == 0 and raw % FILE_ALIGNMENT == 0, name
assert raw_size <= -(-virtual_size // FILE_ALIGNMENT) * FILE_ALIGNMENT, name
assert (raw_size == 0) == (raw == 0) and raw + raw_size <= len(pe), name
mapped = bytearray(image_size)
for name, virtual_size, rva, raw_size, raw in table:
size = min(raw_size, virtual_size)
mapped[rva:rva + size] = pe[raw:raw + size]
loaded = mapped[LOAD_RVA:LOAD_RVA + memory_size]
assert loaded[0x210:0x21a] == image[0x210:0x21a]
assert not any(loaded[0x1000:])
if os.name == "nt":
executed = subprocess.run([str(output)], capture_output=True, text=True, timeout=20)
assert executed.returncode == 42, f"exit code {executed.returncode:#x}"
print("Windows zero-filled memory tests passed")
if __name__ == "__main__":
main()