Seite 1 von 1

DOS-Stub aus EXE/DLL löschen

Verfasst: 09.01.2018, 11:37
von Krishty
Ich habe endlich das Skript wiedergefunden, und damit ich’s selber nicht vergesse, schreibe ich’s auf. Und dann kann ich’s ja direkt mit euch teilen. Also:
  • Windows’ EXE- und DLL-Dateien haben das selbe Format: Portable Executable (PE).
  • PE ist abwärtskompatibel zu MS-DOS. (Bei Microsoft ist immer alles abwärtskompatibel.) Das bedeutet nicht, dass die tatsächlichen Programme auf MS-DOS laufen. Das bedeutet nur, dass MS-DOS moderne EXEs/DLLs lesen und starten kann.
  • Um das zu gewährleisten, wird PE tatsächlich in DOS-Executables (MZ) eingebettet. Das kleine MS-DOS-MZ, in das der moderne Code eingebettet wird, nennt sich MS-DOS Stub. Man *kann* es selber definieren, aber üblicherweise wird es von Compiler erzeugt.
  • Visual C++ erzeugt einen Stub, der – wird die EXE auf MS-DOS gestartet – den folgenden Text ausgibt und dann das Programm beendet: This program cannot be run in DOS mode.
  • Visual C++ kodiert außerdem die verwendete Toolset-Version in das ungenutzte Padding hinter dem MS-DOS-Stub in der PE. Entschlüsselt man das, kann man rekonstruieren, mit welcher Version von Visual C++ die EXE kompiliert wurde.
    MZ header.png
    MZ header.png (4.8 KiB) 6083 mal betrachtet
Ich möchte nun sowohl das MS-DOS-Stub entfernen als auch die Linker-Informationen. Und das am besten automatisch im Build-Skript.
  • Offensichtlich kann man das Stub nicht einfach rausschmeißen, weil die PE in die MZ eingebettet ist, und ohne MZ kann Windows die PE nicht mehr laden. Mann kann es aber minimieren, zu einem leeren Stub.
  • Die Linker-Informationen stehen im Padding, und auch das ist nötigerweise da. Man kann es aber nullen.
Die naheliegende Lösung ist der CFF Explorer:
  • herunterladen
  • installieren
  • CFF Explorer starten
  • EXE öffnen
  • ganz links „Rebuilder“ auswählen
  • cff explorer.png
Das funktioniert: Stub und Linker-Informationen sind tatsächlich gelöscht. Allerdings wurde auch die Ausrichtung der einzelnen Abschnitte innerhalb der PE überschrieben. 512 B sind normalerweise auch Visual C++’ Standard-Ausrichtung, aber der CFF Explorer scheint irgendwie die nicht-ausgerichtete Größe ebenfalls auf den nächsthöheren ausgerichteten Wert zu padden. Das ist kein Problem, die EXE startet weiterhin 100 % normal. Es stört mich aber, weil ich die nicht-ausgerichtete Größe nachschlage, wenn ich den Platzverbrauch meines Codes kontrolliere.

Nun hat der Entwickler von CFF Explorer in seinem Artikel über den MS-DOS-Stub auch ein Skript für den CFF Explorer bereitgestellt, das das gleiche tut, ohne die Ausrichtung zu überschreiben. Dummerweise löscht es das Debug-Verzeichnis und Strong Name Signature mit. Wenn man das alles rausschmeißt, erhält man:

Code: Alles auswählen

-- updated by Krishty 2018
-- do not remove debug directory (I want that for debugging)
-- do not remove strong name signature
-- fixed file name

-- © 2008 Daniel Pistelli. All rights reserved.
-- Header Pack Script Version: 1.0.0.1

-- This neat little script does the following:
--
-- packs the dos header + PE header + section headers
-- removes useless things like the Rich Signature
-- removes linker references inside the PE header
-- strips the debug information (if any) from the PE
-- if it's a .NET, removes Strong Name Signature
-- updates checksum

-- shows an invalid PE Msgbox
function InvPEMsg()
	MsgBox("The current file seems to be an invalid PE. Cannot proceed!",
		strScriptName, MB_ICONEXCLAMATION)
end

-- --------------------------------------------------
-- the main code starts here
-- --------------------------------------------------

strScriptName = "Header Pack Script 1.0.0.1 - by Daniel Pistelli"

filename = @"test.exe"

local hPE = OpenFile(filename)

if hPE == null then
	MsgBox("Couldn't open file.", "Error", MB_ICONEXCLAMATION)
	return
end

local OptHdrOffset = GetOffset(hPE, PE_OptionalHeader)

if OptHdrOffset == null then
	InvPEMsg()
	return
end

-- --------------------------------------------------
-- START PROCESSING
-- --------------------------------------------------

-- PACK HEADERS

do
	local SecrHdrsOffset = GetOffset(hOriginalPE, PE_SectionHeaders)
	
	local SizeOfSections = GetNumberOfSections(hPE) 
		* IMAGE_SIZEOF_SECTION_HEADER
		
	local FileHdrOffset = GetOffset(hPE, PE_FileHeader)
	
	local SizeOfOptionalHdr = ReadWord(hPE, FileHdrOffset + 16)
	
	local SizeOfPEHeader = 4 + 20 + SizeOfOptionalHdr;
	
	local PEHdrOffset = GetOffset(hPE, PE_NtHeaders)
	
	-- we assume that the PE header comes right after
	-- the Dos header, this is a normal PE
	
	FillBytes(hPE, 0x40, PEHdrOffset - 0x40, 0)
	
	-- move headers
	
	local HeadersSize = SizeOfPEHeader + SizeOfSections
	
	local PEHdrAndSects = ReadBytes(hPE, PEHdrOffset, HeadersSize)
	
	FillBytes(hPE, PEHdrOffset, HeadersSize, 0)
	
	WriteBytes(hPE, 0x40, PEHdrAndSects, HeadersSize)
	
	-- e_lfanew
	WriteDword(hPE, 0x3C, 0x40)
	
	OptHdrOffset = GetOffset(hPE, PE_OptionalHeader)
	
end

-- REMOVE LINKER REF

do

	WriteWord(hPE, OptHdrOffset + 2, 0x0000) 

end

-- UPDATE CHECKSUM

UpdateChecksum(hPE)

-- SAVE FILE

SaveFile(hPE)
Um dieses Skript zu benutzen, müsst ihr
  1. in Zeile 30 test.exe durch euren tatsächlichen Dateinamen ersetzen
  2. den CFF Explorer mit dem Skript als Parameter aufrufen
… und das geht wunderbar als Post-Build-Event oder im Deploy-Batch-Skript oder so. Danach ist die EXE „sauber“:
minimal MZ header.png
minimal MZ header.png (1018 Bytes) 6083 mal betrachtet

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 15.01.2018, 02:54
von Krishty
Ich habe auch noch einen Weg gefunden, den PDB-Pfad zu kürzen.

Standardmäßig schreibt Visual C++ den vollen Pfad der Debug-Infos in EXEs/DLLs, auch im Release-Build. Ich möchte das nicht abschalten (oder nachträglich löschen, wie der CFF Explorer erlaubt), weil ich dann keine Crash Dumps von fremden Usern mehr debuggen kann – die Möglichkeit, dann eine gespeicherte PDB zu laden und direkt den Quelltext vor sich zu haben, ist einfach zu wertvoll.

Stattdessen kann Visual C++’ Linker /pdbaltpath:%_PDB% als zusätzlicher Befehl übergeben werden. Dann steht in der EXE/DLL nur noch foo.pdb statt X:\projects\foobar\x64\release\foo.pdb.

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 13.06.2020, 01:45
von Krishty
Ergänzung zum PDB-Tipp: Die DbgHelp-API funktioniert nicht mehr mit dem gekürzten PDB-Pfad, sondern gibt immer nur ERROR_INVALID_ADDRESS zurück. Falls ihr in Debug-Builds DbgHelp braucht (etwa, um Fehlermeldungen mit Bezug zum Quelltext anzuzeigen), rührt die Einstellung nicht an. Ich setze sie nur in Release-Builds.

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 13.03.2021, 13:59
von Zudomon
Krishty hat geschrieben: 15.01.2018, 02:54 Stattdessen kann Visual C++’ Linker /pdbaltpath:%_PDB% als zusätzlicher Befehl übergeben werden. Dann steht in der EXE/DLL nur noch foo.pdb statt X:\projects\foobar\x64\release\foo.pdb.
Kannst du noch schreiben, wo das genau in den Linker-Optionen stehen muss?

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 13.03.2021, 15:35
von Krishty
Projekteigenschaften > Linker > Command Line > Additional Options > da ins Textfeld.

War auch letztens wieder aktuell :D
https://twitter.com/BruceDawson0xB/stat ... 6514837505

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 13.03.2021, 17:02
von Schrompf
Muss ich echt mal tun. Bin aber bissl verunsichert: ich liefere ja meine PDB auch mit der Release-Exe auf Steam aus. Weil ich den Callstack eines Crashes im Log haben will. Das geht zwar gerade nicht, und ich vermute andere Gründe, aber da ich gerade einen seltenen Crash beim Start des Spiels verfolge, will ich gerade ein neues Release mit funktionierendem Crashlog hochladen. Dieses Scheitern der dbghelp.dll passiert nur mit verkürztem PDB-Pfad? Oder ist das allgemein eine Folge des MSDOS-Rauswurfs?

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 13.03.2021, 20:51
von Krishty
DOS-Stub und PDB-Pfad hängen nicht zusammen; die wurden nur zufällig beide von dem Skript gelöscht, und darum habe ich sie hier in einen Topf geworfen. Sorry!

Also … das Scheitern der dbghelp.dll ist eine gute Frage. Wenn du den PDB-Pfad *nicht* trimmst, funktioniert das bei Endkunden wahrscheinlich sowieso nicht, da als PDB-Datei in Splatter C:\Projekte\Splatter\Splatter.pdb verzeichnet ist und dieser Pfad bei Endkunden nicht existiert.

Wenn du /pdbaltpath:%_PDB% anwendest, steht in deiner EXE nur noch Splatter.pdb. Ich hätte erwartet, dass dbghelp.dll die PDB in deinem Programmverzeichnis dann sofort finden kann, aber irgendwie klappte das bei meinen Tests nicht.

Ich bin auch gerade zu eingespannt, da weiter rumzuprobieren, sorry.

Die letzten Jahre habe ich den Call Stack nur in der Form foo.exe+0x12345678 in die Logs geschrieben. Wenn ein User ein Problem hatte, habe ich das Programm im Debugger gestartet und Haltepunkte an diesen Adressen gesetzt. Der Visual C++-Debugger findet die PDB magischerweise (auch mit gekürztem Pfad, falls sie im selben Verzeichnis liegt), damit hatte ich dann also alles, was ich brauche.

Mein Code für einen Call Stack mit reinen Adressen, falls er wem nützt:

Code: Alles auswählen

// replace char16_t with wchar_t, UCount with size_t, toBeginningOf() with std::begin(), and #define WIN32_SHOULD_SUCCEED(CALL) CALL

// Captures the current call stack (excluding this call) as a zero-terminated string.
//  • writes an empty string on failure
//  • layout:
//     our program.exe                  +56789ABC
//     <unknown module>
//     Kernel32.dll                     +56789ABC
//     MaximalLengthIs32CharactersPerF… +56789ABC
//     …
//  • up to 16 entries
void captureCallStack(char16_t result[16 * 45 + 1]) {
	auto toEndOfText = result;

	// Capture the raw call stack:
	void * stackPointers[16];
	auto       toCall = toBeginningOf(stackPointers);
	auto const toEnd  = toCall + RtlCaptureStackBackTrace(1, CAPACITY_OF(stackPointers), stackPointers, nullptr);
	do {
		if(nullptr == *toCall) {
			break; // “GetModuleHandleExW()” does not accept “nullptr”
		}

		// The addresses are useless with Address Space Layout Randomization – instead, get an offset into the module:
		//  • “GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS” allows to pass a code address instead of a string
		//  • “GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT” prevents taking a new reference to the module
		Module * toModule;
		if(WIN32_SHOULD_SUCCEED(GetModuleHandleExW(6, static_cast<char16_t const *>(*toCall), &toModule))) {

			char16_t path[260];
#			if TARGET_OS_VERSION <= TARGET_OS_VERSION_WINDOWS_XP
				// Windows XP does not zero-terminate long paths, so allocate an additional character and zero it manually:
				auto const toEndOfName = path + WIN32_SHOULD_SUCCEED(GetModuleFileNameW(toModule, path, CAPACITY_OF(path) - 1));
				*toEndOfName = u'\0';
#			else
				auto const toEndOfName = path + WIN32_SHOULD_SUCCEED(GetModuleFileNameW(toModule, path, CAPACITY_OF(path)));
#			endif

			// From the path’s end, seek backwards until encountering either
			//  • the path’s beginning
			//  • or a forward or backward slash
			auto toName = toEndOfName;
			for(;;) {
				if(toName == toBeginningOf(path)) {
					break;
				}
				if(*toName == u'\\' || *toName == u'/') {
					++toName; // name begins after the slash
					break;
				}
				--toName;
			}
			auto const nameLength = toEndOfName - toName;

			// Copy the name:
			if(nameLength < 32) {
				toEndOfText = Text::copyUntilZero(toEndOfText, toName);
				toEndOfText = fill(toEndOfText, u' ', static_cast<UCount>(32 - nameLength));
			} else {
				toEndOfText = copyNonOverlapping(toEndOfText, toName, 31);
				*toEndOfText++ = u'\u2026'; // ellipsis
			}

			// Compute the call’s offset in the module:
			//  • always 4-B because even 64-bit Portable Executables are limited to 2³² bytes
			auto const offset = UInt4B(byteDistance(static_cast<void *>(toModule), *toCall));

			// Write the offset:
			*toEndOfText++ = u'+';
			toEndOfText = Text::toHexadecimalZeroPaddedString(toEndOfText, offset);
			*toEndOfText++ = u'\n';

		} else {
			toEndOfText = Text::copyZeroTerminated(toEndOfText, "<unknown module>\n");
		}

	} while(++toCall < toEnd);

	*toEndOfText++ = u'\0'; // we promised a zero terminator
}

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 13.03.2021, 22:49
von Schrompf
Ich habe es heute mal ausprobiert: Exe und PDB ins Steam-Deployment-Verzeichnis kopiert, PDB an der Orginalstelle umbenannt, im Steam-Verzeichnis gestartet und Crash provoziert. Intakter Callstack im Log. Keine Ahnung, warum, aber ich bin erstmal zufrieden.

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 14.03.2021, 10:16
von Schrompf
FICKENMCNUGGETS! Es geht doch nicht. Also auf meinem Rechner schon, aber auf dem Win10-Rechner irgendeines Russen im Steamforum ist das Crashlog immer noch leer.

Das hat alles schonmal funktioniert. Maaaaaan. Ich probiere es jetzt mal mit dem AltPath

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 14.03.2021, 10:24
von Schrompf
Funktioniert übrigens nicht. /pdbaltpath:%_PDB% in die CommandLine des Linkers kopiert, Rebuild All, in der Exe steht immer noch der volle Pfad zur PDB. Mach ich was falsch? Ich guck nur mit ner Dateianzeige.

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 14.03.2021, 10:40
von Zudomon
Bei mir hatte das was Krishty gesagt hatte, geklappt...
Krishty hat geschrieben: 13.03.2021, 15:35 Projekteigenschaften > Linker > Command Line > Additional Options > da ins Textfeld.
Musste zwar etwas suchen, weil das bei mir auf Deutsch gestellt ist. Aber nachdem ich das dann so kompiliert habe, ist der Dateipfad für die PDB aus der EXE verschwunden.

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 14.03.2021, 10:40
von Schrompf
Weil ich blöd bin. Sorry, falscher Alarm, /pdbaltpath:%_PDB% funktioniert doch.

Re: DOS-Stub aus EXE/DLL löschen

Verfasst: 14.03.2021, 10:45
von Schrompf
Bei mir findet dbghlp.dll übrigens trotzdem alle Symbole. In der Exe (mit nem Textanzeiger überprüft) steht jetzt nur noch der Name der PDB. Ich kopiere alle Datenfiles, die Exe und die PDB, in ein Testverzeichnis, lösche nur zur Sicherheit Exe und PDB ausm Buildverzeichnis, starte die Exe und verursache einen Crash. Voller lesbarer Callstack im Log.

Jetzt muss es nur noch auf den Rechnern der Spielerinnen helfen, dann bin ich zufrieden.