blob: 8980f6c2a22d8ff33ecad4b7096fcef94e6d0706 [file]
#!/usr/bin/env python3
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This script compresses a part of shared library without breaking it.
It compresses the specified file range which will be used by a new library
constructor that uses userfaultfd to intercept access attempts to the range
and decompress its data on demand.
Technically this script does the following steps:
1) Makes a copy of specified range, compresses it and adds it to the binary
as a new section using objcopy.
2) Moves the Phdr section to the end of the file to ease next manipulations on
it.
3) Creates a LOAD segment for the compressed section created at step 1.
4) Splits the LOAD segments so the range lives in its own
LOAD segment.
5) Cuts the range out of the binary, correcting offsets, broken in the
process.
6) Changes cut range LOAD segment by zeroing the file_sz and setting
mem_sz to the original range size, essentially lazily zeroing the space.
7) Changes address of the symbols provided by the library constructor to
point to the cut range and compressed section.
"""
import argparse
import logging
import os
import pathlib
import subprocess
import sys
import tempfile
import compression
import elf_headers
COMPRESSED_SECTION_NAME = '.compressed_library_data'
ADDRESS_ALIGN = 0x1000
# src/third_party/llvm-build/Release+Asserts/bin/llvm-objcopy
OBJCOPY_PATH = pathlib.Path(__file__).resolve().parents[3].joinpath(
'third_party/llvm-build/Release+Asserts/bin/llvm-objcopy')
def _SetupLogging():
logging.basicConfig(
format='%(asctime)s %(filename)s:%(lineno)s %(levelname)s] %(message)s',
datefmt='%H:%M:%S',
level=logging.ERROR)
def _ParseArguments():
parser = argparse.ArgumentParser()
parser.add_argument(
'-i', '--input', help='Shared library to parse', required=True)
parser.add_argument(
'-o', '--output', help='Name of the output file', required=True)
parser.add_argument(
'-l',
'--left_range',
help='Beginning of the target part of the library',
type=int,
required=True)
parser.add_argument(
'-r',
'--right_range',
help='End (exclusive) of the target part of the library',
type=int,
required=True)
return parser.parse_args()
def _FileRangeToVirtualAddressRange(data, l, r):
"""Returns virtual address range corresponding to given file range.
Since we have to resolve them by their virtual address, parsing of LOAD
segments is required here.
"""
elf = elf_headers.ElfHeader(data)
for phdr in elf.GetPhdrs():
if phdr.p_type == elf_headers.ProgramHeader.Type.PT_LOAD.value:
# Current version of the prototype only supports ranges which are fully
# contained inside one LOAD segment. It should cover most of the common
# cases.
if phdr.p_offset >= r or phdr.p_offset + phdr.p_filesz <= l:
# Range doesn't overlap.
continue
if phdr.p_offset > l or phdr.p_offset + phdr.p_filesz < r:
# Range overlap with LOAD segment but isn't fully covered by it.
raise RuntimeError('Range is not contained within one LOAD segment')
l_virt = phdr.p_vaddr + (l - phdr.p_offset)
r_virt = phdr.p_vaddr + (r - phdr.p_offset)
return l_virt, r_virt
raise RuntimeError('Specified range is outside of all LOAD segments.')
def _CopyRangeIntoCompressedSection(data, l, r):
"""Adds a new section containing compressed version of provided range."""
virtual_l, virtual_r = _FileRangeToVirtualAddressRange(data, l, r)
# LOAD segments borders are being rounded to the page size so we have to
# shrink [l, r) so corresponding virtual addresses are aligned.
if virtual_l % ADDRESS_ALIGN != 0:
l += ADDRESS_ALIGN - (virtual_l % ADDRESS_ALIGN)
r -= virtual_r % ADDRESS_ALIGN
if l >= r:
raise RuntimeError('Range collapsed after aligning by page size')
compressed_range = compression.CompressData(data[l:r])
with tempfile.TemporaryDirectory() as tmpdir:
# The easiest way to add a new section is to use objcopy, but it requires
# for all of the data to be stored in files.
objcopy_input_file = os.path.join(tmpdir, 'input')
objcopy_data_file = os.path.join(tmpdir, 'data')
objcopy_output_file = os.path.join(tmpdir, 'output')
with open(objcopy_input_file, 'wb') as f:
f.write(data)
with open(objcopy_data_file, 'wb') as f:
f.write(compressed_range)
objcopy_args = [
OBJCOPY_PATH, objcopy_input_file, objcopy_output_file, '--add-section',
'{}={}'.format(COMPRESSED_SECTION_NAME, objcopy_data_file)
]
run_result = subprocess.run(
objcopy_args, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
if run_result.returncode != 0:
raise RuntimeError('objcopy failed with status code {}: {}'.format(
run_result.returncode, run_result.stderr))
with open(objcopy_output_file, 'rb') as f:
data[:] = bytearray(f.read())
def main():
_SetupLogging()
args = _ParseArguments()
with open(args.input, 'rb') as f:
data = f.read()
data = bytearray(data)
_CopyRangeIntoCompressedSection(data, args.left_range, args.right_range)
with open(args.output, 'wb') as f:
f.write(data)
return 0
if __name__ == '__main__':
sys.exit(main())