blob: 8cc2c9e1ea4281eeffed1108c135a546de97613e [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Wraps ml.exe or ml64.exe and postprocesses the output to be deterministic.
Sets timestamp in .obj file to 0, hence incompatible with link.exe /incremental.
Use by prefixing the ml(64).exe invocation with this script:
python ml.py ml.exe [args...]"""
import array
import collections
import struct
import subprocess
import sys
class Struct(object):
"""A thin wrapper around the struct module that returns a namedtuple"""
def __init__(self, name, *args):
"""Pass the name of the return type, and then an interleaved list of
format strings as used by the struct module and of field names."""
self.fmt = '<' + ''.join(args[0::2])
self.type = collections.namedtuple(name, args[1::2])
def pack_into(self, buffer, offset, data):
return struct.pack_into(self.fmt, buffer, offset, *data)
def unpack_from(self, buffer, offset=0):
return self.type(*struct.unpack_from(self.fmt, buffer, offset))
def size(self):
return struct.calcsize(self.fmt)
def Subtract(nt, **kwargs):
"""Subtract(nt, f=2) returns a new namedtuple with 2 subtracted from nt.f"""
return nt._replace(**{k: getattr(nt, k) - v for k, v in kwargs.items()})
def MakeDeterministic(objdata):
# Takes data produced by ml(64).exe (without any special flags) and
# 1. Sets the timestamp to 0
# 2. Strips the .debug$S section (which contains an unwanted absolute path)
# This makes several assumptions about ml's output:
# - Section data is in the same order as the corresponding section headers:
# section headers preceding the .debug$S section header have their data
# preceding the .debug$S section data; likewise for section headers
# following the .debug$S section.
# - The .debug$S section contains only the absolute path to the obj file and
# nothing else, in particular there's only a single entry in the symbol
# table referring to the .debug$S section.
# - There are no COFF line number entries.
# - There's no IMAGE_SYM_CLASS_CLR_TOKEN symbol.
# These seem to hold in practice; if they stop holding this script needs to
# become smarter.
objdata = array.array('b', objdata) # Writable, e.g. via struct.pack_into.
# Read coff header.
COFFHEADER = Struct('COFFHEADER', 'H', 'Machine', 'H', 'NumberOfSections',
'I', 'TimeDateStamp', 'I', 'PointerToSymbolTable', 'I',
'NumberOfSymbols', 'H', 'SizeOfOptionalHeader', 'H',
'Characteristics')
coff_header = COFFHEADER.unpack_from(objdata)
assert coff_header.SizeOfOptionalHeader == 0 # Only set for binaries.
# Read section headers following coff header.
SECTIONHEADER = Struct('SECTIONHEADER', '8s', 'Name', 'I', 'VirtualSize', 'I',
'VirtualAddress', 'I', 'SizeOfRawData', 'I',
'PointerToRawData', 'I', 'PointerToRelocations', 'I',
'PointerToLineNumbers', 'H', 'NumberOfRelocations',
'H', 'NumberOfLineNumbers', 'I', 'Characteristics')
section_headers = []
debug_section_index = -1
for i in range(0, coff_header.NumberOfSections):
section_header = SECTIONHEADER.unpack_from(objdata,
offset=COFFHEADER.size() +
i * SECTIONHEADER.size())
assert not section_header[0].startswith(b'/') # Support short names only.
section_headers.append(section_header)
if section_header.Name == b'.debug$S':
assert debug_section_index == -1
debug_section_index = i
assert debug_section_index != -1
data_start = COFFHEADER.size() + len(section_headers) * SECTIONHEADER.size()
# Verify the .debug$S section looks like we expect.
assert section_headers[debug_section_index].Name == b'.debug$S'
assert section_headers[debug_section_index].VirtualSize == 0
assert section_headers[debug_section_index].VirtualAddress == 0
debug_size = section_headers[debug_section_index].SizeOfRawData
debug_offset = section_headers[debug_section_index].PointerToRawData
assert section_headers[debug_section_index].PointerToRelocations == 0
assert section_headers[debug_section_index].PointerToLineNumbers == 0
assert section_headers[debug_section_index].NumberOfRelocations == 0
assert section_headers[debug_section_index].NumberOfLineNumbers == 0
# Make sure sections in front of .debug$S have their data preceding it.
for header in section_headers[:debug_section_index]:
assert header.PointerToRawData < debug_offset
assert header.PointerToRelocations < debug_offset
assert header.PointerToLineNumbers < debug_offset
# Make sure sections after of .debug$S have their data following it.
for header in section_headers[debug_section_index + 1:]:
# Make sure the .debug$S data is at the very end of section data:
assert header.PointerToRawData > debug_offset
assert header.PointerToRelocations == 0
assert header.PointerToLineNumbers == 0
# Make sure the first non-empty section's data starts right after the section
# headers.
for section_header in section_headers:
if section_header.PointerToRawData == 0:
assert section_header.PointerToRelocations == 0
assert section_header.PointerToLineNumbers == 0
continue
assert section_header.PointerToRawData == data_start
break
# Make sure the symbol table (and hence, string table) appear after the last
# section:
assert (
coff_header.PointerToSymbolTable >=
section_headers[-1].PointerToRawData + section_headers[-1].SizeOfRawData)
# The symbol table contains a symbol for the no-longer-present .debug$S
# section. If we leave it there, lld-link will complain:
#
# lld-link: error: .debug$S should not refer to non-existent section 5
#
# so we need to remove that symbol table entry as well. This shifts symbol
# entries around and we need to update symbol table indices in:
# - relocations
# - line number records (never present)
# - one aux symbol entry (IMAGE_SYM_CLASS_CLR_TOKEN; not present in ml output)
SYM = Struct(
'SYM',
'8s',
'Name',
'I',
'Value',
'h',
'SectionNumber', # Note: Signed!
'H',
'Type',
'B',
'StorageClass',
'B',
'NumberOfAuxSymbols')
i = 0
debug_sym = -1
while i < coff_header.NumberOfSymbols:
sym_offset = coff_header.PointerToSymbolTable + i * SYM.size()
sym = SYM.unpack_from(objdata, sym_offset)
# 107 is IMAGE_SYM_CLASS_CLR_TOKEN, which has aux entry "CLR Token
# Definition", which contains a symbol index. Check it's never present.
assert sym.StorageClass != 107
# Note: sym.SectionNumber is 1-based, debug_section_index is 0-based.
if sym.SectionNumber - 1 == debug_section_index:
assert debug_sym == -1, 'more than one .debug$S symbol found'
debug_sym = i
# Make sure the .debug$S symbol looks like we expect.
# In particular, it should have exactly one aux symbol.
assert sym.Name == b'.debug$S'
assert sym.Value == 0
assert sym.Type == 0
assert sym.StorageClass == 3
assert sym.NumberOfAuxSymbols == 1
elif sym.SectionNumber > debug_section_index:
sym = Subtract(sym, SectionNumber=1)
SYM.pack_into(objdata, sym_offset, sym)
i += 1 + sym.NumberOfAuxSymbols
assert debug_sym != -1, '.debug$S symbol not found'
# Note: Usually the .debug$S section is the last, but for files saying
# `includelib foo.lib`, like safe_terminate_process.asm in 32-bit builds,
# this isn't true: .drectve is after .debug$S.
# Update symbol table indices in relocations.
# There are a few processor types that have one or two relocation types
# where SymbolTableIndex has a different meaning, but not for x86.
REL = Struct('REL', 'I', 'VirtualAddress', 'I', 'SymbolTableIndex', 'H',
'Type')
for header in section_headers[0:debug_section_index]:
for j in range(0, header.NumberOfRelocations):
rel_offset = header.PointerToRelocations + j * REL.size()
rel = REL.unpack_from(objdata, rel_offset)
assert rel.SymbolTableIndex != debug_sym
if rel.SymbolTableIndex > debug_sym:
rel = Subtract(rel, SymbolTableIndex=2)
REL.pack_into(objdata, rel_offset, rel)
# Update symbol table indices in line numbers -- just check they don't exist.
for header in section_headers:
assert header.NumberOfLineNumbers == 0
# Now that all indices are updated, remove the symbol table entry referring to
# .debug$S and its aux entry.
del objdata[coff_header.PointerToSymbolTable +
debug_sym * SYM.size():coff_header.PointerToSymbolTable +
(debug_sym + 2) * SYM.size()]
# Now we know that it's safe to write out the input data, with just the
# timestamp overwritten to 0, the last section header cut out (and the
# offsets of all other section headers decremented by the size of that
# one section header), and the last section's data cut out. The symbol
# table offset needs to be reduced by one section header and the size of
# the missing section.
# (The COFF spec only requires on-disk sections to be aligned in image files,
# for obj files it's not required. If that wasn't the case, deleting slices
# if data would not generally be safe.)
# Update section offsets and remove .debug$S section data.
for i in range(0, debug_section_index):
header = section_headers[i]
if header.SizeOfRawData:
header = Subtract(header, PointerToRawData=SECTIONHEADER.size())
if header.NumberOfRelocations:
header = Subtract(header, PointerToRelocations=SECTIONHEADER.size())
if header.NumberOfLineNumbers:
header = Subtract(header, PointerToLineNumbers=SECTIONHEADER.size())
SECTIONHEADER.pack_into(objdata,
COFFHEADER.size() + i * SECTIONHEADER.size(),
header)
for i in range(debug_section_index + 1, len(section_headers)):
header = section_headers[i]
shift = SECTIONHEADER.size() + debug_size
if header.SizeOfRawData:
header = Subtract(header, PointerToRawData=shift)
if header.NumberOfRelocations:
header = Subtract(header, PointerToRelocations=shift)
if header.NumberOfLineNumbers:
header = Subtract(header, PointerToLineNumbers=shift)
SECTIONHEADER.pack_into(objdata,
COFFHEADER.size() + i * SECTIONHEADER.size(),
header)
del objdata[debug_offset:debug_offset + debug_size]
# Finally, remove .debug$S section header and update coff header.
coff_header = coff_header._replace(TimeDateStamp=0)
coff_header = Subtract(coff_header,
NumberOfSections=1,
PointerToSymbolTable=SECTIONHEADER.size() + debug_size,
NumberOfSymbols=2)
COFFHEADER.pack_into(objdata, 0, coff_header)
del objdata[COFFHEADER.size() +
debug_section_index * SECTIONHEADER.size():COFFHEADER.size() +
(debug_section_index + 1) * SECTIONHEADER.size()]
# All done!
if sys.version_info.major == 2:
return objdata.tostring()
else:
return objdata.tobytes()
def main():
ml_result = subprocess.call(sys.argv[1:])
if ml_result != 0:
return ml_result
objfile = None
for i in range(1, len(sys.argv)):
if sys.argv[i].startswith('/Fo'):
objfile = sys.argv[i][len('/Fo'):]
assert objfile, 'failed to find ml output'
with open(objfile, 'rb') as f:
objdata = f.read()
objdata = MakeDeterministic(objdata)
with open(objfile, 'wb') as f:
f.write(objdata)
if __name__ == '__main__':
sys.exit(main())