go icon indicating copy to clipboard operation
go copied to clipboard

proposal: debug/pe: add BigObj COFF support via new fields on File struct

Open Peter0x44 opened this issue 3 months ago • 18 comments

Proposal Details

The debug/pe package currently uses this file struct:

// A File represents an open PE file.
type File struct {
	FileHeader
	OptionalHeader any // of type *OptionalHeader32 or *OptionalHeader64
	Sections       []*Section
	Symbols        []*Symbol    // COFF symbols with auxiliary symbol records removed
	COFFSymbols    []COFFSymbol // all COFF symbols (including auxiliary symbol records)
	StringTable    StringTable

	closer io.Closer
}

Unfortunately, in order to support BigObj COFF, (golang/go#24341), a few things need to change,. BigObj files use a different type of header (ANON_OBJECT_HEADER_BIGOBJ) with largely the same fields as the FileHeader (IMAGE_FILE_HEADER) present already in the File struct.

You can find the C struct declarations in winnt.h.

The most important difference in the header is that FileHeader.NumberOfSections is now 32 bits, instead of 16 bits. Supporting more sections in the file is the entire point of BigObj. I propose to add an extra: int32 SectionCount field in the file struct to deal with this. In the case that the section count is greater than INT16_MAX, File.FileHeader.NumberOfSections will be zero. File.SectionCount will always contain an accurate number of sections.

File.FileHeader.NumberOfSections will be deprecated and new code should not rely on it, and instead refer to File.SectionCount to get the true number of sections.

The other fields in the header have 1:1 equivalents in the old FileHeader, so the code can just copy over the values from the BigObj header back to the FileHeader, and they can be accessed from that struct instead.

A similar approach has to be taken for Symbols and COFFSymbols.

Symbols inside regular COFF files are of the format IMAGE_SYMBOL (also present in winnt.h). BigObj files instead have the format IMAGE_SYMBOL_EX. The only difference between them is that the SectionNumber field got expanded to 32 bits. Unfortunately, the current COFFSymbol and Symbol structs have a 16 bit SectionNumber.

I propose adding two new fields:

BigObjSymbols []*BigObjSymbol
BigObjCOFFSymbols []BigObjCOFFSymbol

The only change in these structs will be a 32 bit SectionNumber.

Similar to the header, the old structs will be deprecated, and both of those arrays will be empty in the case that the section count is greater than INT16_MAX. New code should always prefer to use these fields.

For more details on BigObj, I wrote up a blog post explaining my findings when writing my initial patch: https://peter0x44.github.io/posts/bigobj_format_explained/ Unfortunately, Microsoft doesn't document the binary format. And it seems no one else did either. So you just have to "Trust Me, Bro". https://github.com/golang/go/pull/75631

My patch is functional. The issues are that it breaks the go compatibility guarantee.

I am happy to bikeshed over the naming of the structs in this API. I couldn't come up with any I fully liked.

Peter0x44 avatar Sep 29 '25 23:09 Peter0x44

cc @ianlancetaylor

Peter0x44 avatar Sep 29 '25 23:09 Peter0x44

CC @golang/windows

There are two parts to this proposal.

The first part is to add a new field to pe.File to hold the 32-bit number of sections. Currently pe.File embeds pe.FileHeader, and pe.FileHeader has the field NumberOfSections uint16. The proposal is to add a new field to pe.File (not pe.FileHeader): SectionCount uint32. But actually I'm not sure we need that field. I think that the field is redundant with the length of the Sections field. I think that we can instead tell people to use len(f.Sections) rather than f.NumberOfSections to get the number of sections.

Unfortunately, I don't know that we have a way to formally deprecate a field inherited from an embedded struct. There is nothing wrong with the FileHeader.NumberOfSections field; that field is correct in the cases where the FileHeader struct is correct. The problem is with the File struct, which should be able to represent both traditional PE files and BigObj files.

The second part of the proposal is to handle the section number of a symbol. Currently the type Symbol has a field SectionNumber int16. The proposal above suggests adding new BigObjSymbol and BigObjCoffSymbol types.

I don't think we need BigObjSymbol; the Symbol type does not correspond to any data structure in a PE file. I think we can add a new field to Symbol: BigSectionNumber uint32. Then we can deprecate SectionNumber in favor of that field.

It's less clear if we can do this for COFFSymbol. That type does currently correspond to a type in the file. Still, we could decide to break that correspondence and add a BigSectionNumber field to that type as well. And, for that matter, to COFFSymbolAuxFormat5, which is returned by the (*File).COFFSymbolReadSectionDefAux method. This would complicate the code in the debug/pe package, but would simplify the code that uses it. It would be unfortunate if users would have to switch from the Symbols slice to a BigObjSymbols slice. It would be especially unfortunate if they have to always switch. Bad enough to force people to switch from SectionNumber to BigSectionNumber.

ianlancetaylor avatar Sep 30 '25 04:09 ianlancetaylor

This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — aclements for the proposal review group

aclements avatar Oct 01 '25 23:10 aclements

In general this seems like a good thing to support, but we need a concrete proposal that people are generally agreed on.

aclements avatar Oct 08 '25 17:10 aclements

A concrete proposal:

  • Document that the NumberOfSections field of debug/pe.File, which is a field inherited from the embedded type debug/pe.FileHeader, is not necessarily accurate. People who need to know the number of sections given a debug/pe.File should instead use len of the Sections field.
  • Add a new field FullSectionNumber, of type uint32, to debug/pe.Symbol. Always set it to the symbol's section number. Document that the existing SectionNumber field is incorrect if there are more than 65536 sections.
  • Similarly, add a new field FullSectionNumber, of type uint32, to debug/pe.COFFSymbol.
  • Similarly, add a new field FullSectNum, of type uint32, to debug/pe.COFFSymbolAuxFormat5.

I think those are the minimal changes we need to make it possible to read bigobj files.

ianlancetaylor avatar Oct 08 '25 21:10 ianlancetaylor

Most of that proposal seems okay to me. But there's a few things of note I'd like to point out

Perhaps uint32 is not the correct type for these. Microsoft documents a few negative section numbers as being meaningful, here: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-number-values

Now for maybe a little TMI from my results of testing various tools. The number of sections is not limited to 65535 for regular COFF. It's a little less.

65,279 is what they document. It seems to be accurate. I did some testing with cl, binutils, and clang, and learned the following: The maximum Binutils as will allow is 32766 The maximum Microsoft's assembler "ml64" will allow is 32767 The maximum cl.exe will allow is 65279. The same applies for clang. But instead of erroring, it will write out a bigobj coff instead, if 65279 is exceeded.

For bigobj, Microsoft document a limit of 4,294,967,296 (2^32-1). I could not test this because compiling/assembling such a file takes too long.

Peter0x44 avatar Oct 08 '25 21:10 Peter0x44

I'm fine with using int32 or int rather than uint32. Thanks.

  • Document that the NumberOfSections field of debug/pe.File, which is a field inherited from the embedded type debug/pe.FileHeader, is not necessarily accurate. People who need to know the number of sections given a debug/pe.File should instead use len of the Sections field.
  • Add a new field FullSectionNumber, of type int32, to debug/pe.Symbol. Always set it to the symbol's section number. Document that the existing SectionNumber field is incorrect if there are more than 65536 sections.
  • Similarly, add a new field FullSectionNumber, of type int32, to debug/pe.COFFSymbol.
  • Similarly, add a new field FullSectNum, of type int32, to debug/pe.COFFSymbolAuxFormat5.

ianlancetaylor avatar Oct 09 '25 02:10 ianlancetaylor

Well, on further thought, I'm not sure if Microsoft is expecting the section count to be signed or unsigned. Or signed, but you cast to unsigned. Since 65,279 is larger than INT16_MAX. a (D)WORD is signed. But they also do document the negative section numbers. So I'm confused.

Peter0x44 avatar Oct 09 '25 11:10 Peter0x44

Well okay, thinking about it further, "count" and "number" are two distinct things technically. So Int32 for FullSectionNumber is correct. But NumberOfSections should be unsigned. But it doesn't matter since we are telling people to use len(Sections) anyway.

So this proposal:

  • Document that the NumberOfSections field of debug/pe.File, which is a field inherited from the embedded type debug/pe.FileHeader, is not necessarily accurate. People who need to know the number of sections given a debug/pe.File should instead use len of the Sections field.
  • Add a new field FullSectionNumber, of type int32, to debug/pe.Symbol. Always set it to the symbol's section number. Document that the existing SectionNumber field is incorrect if there are more than 65536 sections.
  • Similarly, add a new field FullSectionNumber, of type int32, to debug/pe.COFFSymbol.
  • Similarly, add a new field FullSectNum, of type int32, to debug/pe.COFFSymbolAuxFormat5.

is OK. Approved by me.

Peter0x44 avatar Oct 11 '25 23:10 Peter0x44

Similarly, add a new field FullSectionNumber, of type int32, to debug/pe.COFFSymbol. Similarly, add a new field FullSectNum, of type int32, to debug/pe.COFFSymbolAuxFormat5.

I arrived at the same conclusion that @ianlancetaylor did up thread that both of these layouts are currently disk-compatible, read using binary.Read.

Changing this could potentially break some callers, for example, if any assume they can binary.Write these types. Maybe that doesn't happen in practice.

I'm more worried about File.COFFSymbols, which has type []COFFSymbol, but includes both COFFSymbol values and COFFSymbolAuxFormat5 values, and code assumes it can unsafe cast between them. This is marginally excusable in a world where they're just the on-disk structures because those are guaranteed to be the same byte size. At a minimum, if we do change them, we need to test that they're the same byte size. But in general this makes me reticent to change COFFSymbol and COFFSymbolAuxFormat5.

aclements avatar Oct 15 '25 16:10 aclements

Fair points. Unfortunately that leaves us with no good approach. Perhaps we could define additional File methods and tell people to never use the COFFSymbols field.

ianlancetaylor avatar Oct 15 '25 23:10 ianlancetaylor

For bigobj, Microsoft document a limit of 4,294,967,296 (2^32-1).

This sounds like it either needs to be a uint32 or a type with an even larger range, like int64. int64 would also mean we don't have to have this conversation again in 10 years when Microsoft defines ReallyBigCOFF :)

How are IMAGE_SYM_ABSOLUTE (-1) and IMAGE_SYM_DEBUG (-2) encoded in BigCOFF?

aclements avatar Oct 22 '25 16:10 aclements

The count is unsigned (despite being a DWORD...) but I think the section number/ID is signed. That's how I figure it works. But I will see how I can test it out.

Peter0x44 avatar Oct 22 '25 18:10 Peter0x44

How are IMAGE_SYM_ABSOLUTE (-1) and IMAGE_SYM_DEBUG (-2) encoded in BigCOFF?

OK. I got around to testing it, and the result is unsurprising.

0xFFFFFFFFFF for IMAGE_SYM_ABSOLUTE and 0xFEFFFFFF for IMAGE_SYM_DEBUG. Of course, the second is -2 in little endian.

Peter0x44 avatar Nov 02 '25 22:11 Peter0x44

int64 would also mean we don't have to have this conversation again in 10 years when Microsoft defines ReallyBigCOFF :)

If this ever happens, we are probably getting a whole new executable format for Windows that isn't limited to 4GiB in the first place. And also, having dlls that can export more than 65535 symbols.

Peter0x44 avatar Nov 02 '25 22:11 Peter0x44

So, what should I implement? I think this will probably remain stalled indefinitely if I don't prompt it again...

Peter0x44 avatar Dec 05 '25 14:12 Peter0x44

Sounds like we should use int64.

ianlancetaylor avatar Dec 05 '25 18:12 ianlancetaylor

The int64 was a minor problem. But we never found a way to address my concern about changing COFFSymbol and COFFSymbolAuxFormat5 given the unsafe casts between them and current disk-compatibility.

aclements avatar Dec 17 '25 17:12 aclements