You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
267 lines
12 KiB
267 lines
12 KiB
#!/usr/bin/env python3
|
|
# Copyright 2020 The Pigweed Authors
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
# use this file except in compliance with the License. You may obtain a copy of
|
|
# the License at
|
|
#
|
|
# https://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations under
|
|
# the License.
|
|
"""Tests the ELF reader Python module."""
|
|
|
|
import io
|
|
import os
|
|
import re
|
|
import unittest
|
|
|
|
from pw_tokenizer import elf_reader
|
|
|
|
# Output from the following command:
|
|
#
|
|
# readelf -WS elf_reader_test_binary.elf
|
|
#
|
|
TEST_READELF_OUTPUT = ("""
|
|
There are 33 section headers, starting at offset 0x1758:
|
|
|
|
Section Headers:
|
|
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
|
|
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
|
|
[ 1] .interp PROGBITS 0000000000000238 000238 00001c 00 A 0 0 1
|
|
[ 2] .note.ABI-tag NOTE 0000000000000254 000254 000020 00 A 0 0 4
|
|
[ 3] .note.gnu.build-id NOTE 0000000000000274 000274 000024 00 A 0 0 4
|
|
[ 4] .dynsym DYNSYM 0000000000000298 000298 0000a8 18 A 5 1 8
|
|
[ 5] .dynstr STRTAB 0000000000000340 000340 00009b 00 A 0 0 1
|
|
[ 6] .gnu.hash GNU_HASH 00000000000003e0 0003e0 00001c 00 A 4 0 8
|
|
[ 7] .gnu.version VERSYM 00000000000003fc 0003fc 00000e 02 A 4 0 2
|
|
[ 8] .gnu.version_r VERNEED 000000000000040c 00040c 000020 00 A 5 1 4
|
|
[ 9] .rela.dyn RELA 0000000000000430 000430 0000d8 18 A 4 0 8
|
|
[10] .rela.plt RELA 0000000000000508 000508 000018 18 AI 4 12 8
|
|
[11] .init PROGBITS 0000000000000520 000520 000017 00 AX 0 0 4
|
|
[12] .plt PROGBITS 0000000000000540 000540 000020 10 AX 0 0 16
|
|
[13] .text PROGBITS 0000000000000560 000560 000151 00 AX 0 0 16
|
|
[14] .fini PROGBITS 00000000000006b4 0006b4 000009 00 AX 0 0 4
|
|
[15] .rodata PROGBITS 00000000000006c0 0006c0 000004 04 AM 0 0 4
|
|
[16] .test_section_1 PROGBITS 00000000000006d0 0006d0 000010 00 A 0 0 16
|
|
[17] .test_section_2 PROGBITS 00000000000006e0 0006e0 000004 00 A 0 0 4
|
|
[18] .eh_frame X86_64_UNWIND 00000000000006e8 0006e8 0000d4 00 A 0 0 8
|
|
[19] .eh_frame_hdr X86_64_UNWIND 00000000000007bc 0007bc 00002c 00 A 0 0 4
|
|
[20] .fini_array FINI_ARRAY 0000000000001d80 000d80 000008 08 WA 0 0 8
|
|
[21] .init_array INIT_ARRAY 0000000000001d88 000d88 000008 08 WA 0 0 8
|
|
[22] .dynamic DYNAMIC 0000000000001d90 000d90 000220 10 WA 5 0 8
|
|
[23] .got PROGBITS 0000000000001fb0 000fb0 000030 00 WA 0 0 8
|
|
[24] .got.plt PROGBITS 0000000000001fe0 000fe0 000020 00 WA 0 0 8
|
|
[25] .data PROGBITS 0000000000002000 001000 000010 00 WA 0 0 8
|
|
[26] .tm_clone_table PROGBITS 0000000000002010 001010 000000 00 WA 0 0 8
|
|
[27] .bss NOBITS 0000000000002010 001010 000001 00 WA 0 0 1
|
|
[28] .comment PROGBITS 0000000000000000 001010 00001d 01 MS 0 0 1
|
|
[29] .note.gnu.gold-version NOTE 0000000000000000 001030 00001c 00 0 0 4
|
|
[30] .symtab SYMTAB 0000000000000000 001050 000390 18 31 21 8
|
|
[31] .strtab STRTAB 0000000000000000 0013e0 000227 00 0 0 1
|
|
[32] .shstrtab STRTAB 0000000000000000 001607 00014a 00 0 0 1
|
|
Key to Flags:
|
|
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
|
|
L (link order), O (extra OS processing required), G (group), T (TLS),
|
|
C (compressed), x (unknown), o (OS specific), E (exclude),
|
|
l (large), p (processor specific)
|
|
""")
|
|
|
|
TEST_ELF_PATH = os.path.join(os.path.dirname(__file__),
|
|
'elf_reader_test_binary.elf')
|
|
|
|
|
|
class ElfReaderTest(unittest.TestCase):
|
|
"""Tests the elf_reader.Elf class."""
|
|
def setUp(self):
|
|
super().setUp()
|
|
self._elf_file = open(TEST_ELF_PATH, 'rb')
|
|
self._elf = elf_reader.Elf(self._elf_file)
|
|
|
|
def tearDown(self):
|
|
super().tearDown()
|
|
self._elf_file.close()
|
|
|
|
def _section(self, name):
|
|
return next(self._elf.sections_with_name(name))
|
|
|
|
def test_readelf_comparison_using_the_readelf_binary(self):
|
|
"""Compares elf_reader to readelf's output."""
|
|
|
|
parse_readelf_output = re.compile(r'\s+'
|
|
r'\[\s*(?P<number>\d+)\]\s+'
|
|
r'(?P<name>\.\S*)?\s+'
|
|
r'(?P<type>\S+)\s+'
|
|
r'(?P<addr>[0-9a-fA-F]+)\s+'
|
|
r'(?P<offset>[0-9a-fA-F]+)\s+'
|
|
r'(?P<size>[0-9a-fA-F]+)\s+')
|
|
|
|
readelf_sections = []
|
|
for number, name, _, addr, offset, size in parse_readelf_output.findall(
|
|
TEST_READELF_OUTPUT):
|
|
readelf_sections.append((
|
|
int(number),
|
|
name or '',
|
|
int(addr, 16),
|
|
int(offset, 16),
|
|
int(size, 16),
|
|
))
|
|
|
|
self.assertEqual(len(readelf_sections), 33)
|
|
self.assertEqual(len(readelf_sections), len(self._elf.sections))
|
|
|
|
for (index,
|
|
section), readelf_section in zip(enumerate(self._elf.sections),
|
|
readelf_sections):
|
|
readelf_index, name, address, offset, size = readelf_section
|
|
|
|
self.assertEqual(index, readelf_index)
|
|
self.assertEqual(section.name, name)
|
|
self.assertEqual(section.address, address)
|
|
self.assertEqual(section.offset, offset)
|
|
self.assertEqual(section.size, size)
|
|
|
|
def test_dump_single_section(self):
|
|
self.assertEqual(self._elf.dump_section_contents(r'\.test_section_1'),
|
|
b'You cannot pass\0')
|
|
self.assertEqual(self._elf.dump_section_contents(r'\.test_section_2'),
|
|
b'\xef\xbe\xed\xfe')
|
|
|
|
def test_dump_multiple_sections(self):
|
|
if (self._section('.test_section_1').address <
|
|
self._section('.test_section_2').address):
|
|
contents = b'You cannot pass\0\xef\xbe\xed\xfe'
|
|
else:
|
|
contents = b'\xef\xbe\xed\xfeYou cannot pass\0'
|
|
|
|
self.assertIn(self._elf.dump_section_contents(r'.test_section_\d'),
|
|
contents)
|
|
|
|
def test_read_values(self):
|
|
address = self._section('.test_section_1').address
|
|
self.assertEqual(self._elf.read_value(address), b'You cannot pass')
|
|
|
|
int32_address = self._section('.test_section_2').address
|
|
self.assertEqual(self._elf.read_value(int32_address, 4),
|
|
b'\xef\xbe\xed\xfe')
|
|
|
|
def test_read_string(self):
|
|
bytes_io = io.BytesIO(
|
|
b'This is a null-terminated string\0No terminator!')
|
|
self.assertEqual(elf_reader.read_c_string(bytes_io),
|
|
b'This is a null-terminated string')
|
|
self.assertEqual(elf_reader.read_c_string(bytes_io), b'No terminator!')
|
|
self.assertEqual(elf_reader.read_c_string(bytes_io), b'')
|
|
|
|
def test_compatible_file_for_elf(self):
|
|
self.assertTrue(elf_reader.compatible_file(self._elf_file))
|
|
self.assertTrue(elf_reader.compatible_file(io.BytesIO(b'\x7fELF')))
|
|
|
|
def test_compatible_file_for_elf_start_at_offset(self):
|
|
self._elf_file.seek(13) # Seek ahead to get out of sync
|
|
self.assertTrue(elf_reader.compatible_file(self._elf_file))
|
|
self.assertEqual(13, self._elf_file.tell())
|
|
|
|
def test_compatible_file_for_invalid_elf(self):
|
|
self.assertFalse(elf_reader.compatible_file(io.BytesIO(b'\x7fELVESF')))
|
|
|
|
|
|
def _archive_file(data: bytes) -> bytes:
|
|
return ('FILE ID 90123456'
|
|
'MODIFIED 012'
|
|
'OWNER '
|
|
'GROUP '
|
|
'MODE 678'
|
|
f'{len(data):10}' # File size -- the only part that's needed.
|
|
'`\n'.encode() + data)
|
|
|
|
|
|
class ArchiveTest(unittest.TestCase):
|
|
"""Tests reading from archive files."""
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
with open(TEST_ELF_PATH, 'rb') as fd:
|
|
self._elf_data = fd.read()
|
|
|
|
self._archive_entries = b'blah', b'hello', self._elf_data
|
|
|
|
self._archive_data = elf_reader.ARCHIVE_MAGIC + b''.join(
|
|
_archive_file(f) for f in self._archive_entries)
|
|
self._archive = io.BytesIO(self._archive_data)
|
|
|
|
def test_compatible_file_for_archive(self):
|
|
self.assertTrue(elf_reader.compatible_file(io.BytesIO(b'!<arch>\n')))
|
|
self.assertTrue(elf_reader.compatible_file(self._archive))
|
|
|
|
def test_compatible_file_for_invalid_archive(self):
|
|
self.assertFalse(elf_reader.compatible_file(io.BytesIO(b'!<arch>')))
|
|
|
|
def test_iterate_over_files(self):
|
|
for expected, size in zip(self._archive_entries,
|
|
elf_reader.files_in_archive(self._archive)):
|
|
self.assertEqual(expected, self._archive.read(size))
|
|
|
|
def test_iterate_over_empty_archive(self):
|
|
with self.assertRaises(StopIteration):
|
|
next(iter(elf_reader.files_in_archive(io.BytesIO(b'!<arch>\n'))))
|
|
|
|
def test_iterate_over_invalid_archive(self):
|
|
with self.assertRaises(elf_reader.FileDecodeError):
|
|
for _ in elf_reader.files_in_archive(
|
|
io.BytesIO(b'!<arch>blah blahblah')):
|
|
pass
|
|
|
|
def test_extra_newline_after_entry_is_ignored(self):
|
|
archive = io.BytesIO(elf_reader.ARCHIVE_MAGIC +
|
|
_archive_file(self._elf_data) + b'\n' +
|
|
_archive_file(self._elf_data))
|
|
|
|
for size in elf_reader.files_in_archive(archive):
|
|
self.assertEqual(self._elf_data, archive.read(size))
|
|
|
|
def test_two_extra_newlines_parsing_fails(self):
|
|
archive = io.BytesIO(elf_reader.ARCHIVE_MAGIC +
|
|
_archive_file(self._elf_data) + b'\n\n' +
|
|
_archive_file(self._elf_data))
|
|
|
|
with self.assertRaises(elf_reader.FileDecodeError):
|
|
for size in elf_reader.files_in_archive(archive):
|
|
self.assertEqual(self._elf_data, archive.read(size))
|
|
|
|
def test_iterate_over_archive_with_invalid_size(self):
|
|
data = elf_reader.ARCHIVE_MAGIC + _archive_file(b'$' * 3210)
|
|
file = io.BytesIO(data)
|
|
|
|
# Iterate over the file normally.
|
|
for size in elf_reader.files_in_archive(file):
|
|
self.assertEqual(b'$' * 3210, file.read(size))
|
|
|
|
# Replace the size with a hex number, which is not valid.
|
|
with self.assertRaises(elf_reader.FileDecodeError):
|
|
for _ in elf_reader.files_in_archive(
|
|
io.BytesIO(data.replace(b'3210', b'0x99'))):
|
|
pass
|
|
|
|
def test_elf_reader_dump_single_section(self):
|
|
elf = elf_reader.Elf(self._archive)
|
|
self.assertEqual(elf.dump_section_contents(r'\.test_section_1'),
|
|
b'You cannot pass\0')
|
|
self.assertEqual(elf.dump_section_contents(r'\.test_section_2'),
|
|
b'\xef\xbe\xed\xfe')
|
|
|
|
def test_elf_reader_read_values(self):
|
|
elf = elf_reader.Elf(self._archive)
|
|
address = next(elf.sections_with_name('.test_section_1')).address
|
|
self.assertEqual(elf.read_value(address), b'You cannot pass')
|
|
|
|
int32_address = next(elf.sections_with_name('.test_section_2')).address
|
|
self.assertEqual(elf.read_value(int32_address, 4), b'\xef\xbe\xed\xfe')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|