[Bug 48898] 4k demoscene OpenGL demo 'End of time' by Alcatraz and Altair crashes on startup due to missing opengl32.dll 'wglGetDefaultProcAddress' stub

WineHQ Bugzilla wine-bugs at winehq.org
Mon Apr 13 16:01:39 CDT 2020


https://bugs.winehq.org/show_bug.cgi?id=48898

Anastasius Focht <focht at gmx.net> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
            Summary|4k demoscene OpenGL demo    |4k demoscene OpenGL demo
                   |'End of time' by Alcatraz   |'End of time' by Alcatraz
                   |and Altair crashes on       |and Altair crashes on
                   |startup due to opengl32.dll |startup due to missing
                   |imports ordinal mismatch    |opengl32.dll
                   |                            |'wglGetDefaultProcAddress'
                   |                            |stub

--- Comment #5 from Anastasius Focht <focht at gmx.net> ---
Hello Vijay,

--- quote ---
With the relay log and the patch from #42125 can we find the ordinals the demo
uses?
--- quote ---

well, no. It's actually more complicated. The Crinkler executable file
compressor implements a custom loader/imports resolver which doesn't use any
win32 API calls for resolving. Since everything needs to be kept small, it uses
a pretty clever approach, consuming only a minimum amount of information/data
storage.

--- snip ---
$ WINEDEBUG=+seh,+relay,+imports,+module,+opengl,+wgl wine ./End\ of\ time\
720p.exe >>log.txt 2>&1
...
0009:Call KERNEL32.LoadLibraryA(004208eb "opengl32") ret=004200a4
...
0009:Ret  KERNEL32.LoadLibraryA() retval=7a840000 ret=004200a4
...
0009:Call opengl32.wglMakeCurrent(00421314,00008b30) ret=00420814
0009:Ret  opengl32.wglMakeCurrent() retval=00000000 ret=00420814
0009:trace:seh:raise_exception code=c0000005 flags=0 addr=(nil) ip=00000000
tid=0009
0009:trace:seh:raise_exception  info[0]=00000000
0009:trace:seh:raise_exception  info[1]=00000000
0009:trace:seh:raise_exception  eax=00000000 ebx=00400148 ecx=00421250
edx=00000000 esi=7a8477cc edi=00030039
0009:trace:seh:raise_exception  ebp=1518ff00 esp=1518fee4 cs=0023 ds=002b
es=002b fs=0063 gs=006b flags=00010202
0009:trace:seh:call_stack_handlers calling handler at 0x7b469400 code=c0000005
flags=0 
--- snip ---

As already said, relay trace yields no useful information. One has to debug it.

The easiest way to debug Crinkler executables is to set a hardware breakpoint
on execution at the decompression area for code. It's usually located at fixed
0x420000 address range for 32-bit.

After hitting the breakpoint you will come across the following code. I've
annotated the relevant disassembly snippet but will also describe it in a
summary after the snippet in case assembly it not your cup of tea ;-)

--- snip ---
; uncompressed entry
00420000  POP EDI
00420001  MOV ECX,1D
00420006  MOV AL,0E8
...
00420018  MOV EBX,OFFSET 00400108       ; hash names start
0042001D  MOV ESI,OFFSET 004208CE       ; hash imports ranges
00420022  MOV EDI,OFFSET 00430000       ; area for custom IAT
00420027  POP EAX                       ; PEB
00420028  MOV EAX,DWORD PTR DS:[EAX+0C] ; PEB_LDR_DATA
0042002B  MOV EAX,DWORD PTR DS:[EAX+0C] 
0042002E  MOV EAX,DWORD PTR DS:[EAX]
00420030  MOV EAX,DWORD PTR DS:[EAX]    ; Flink
; _LDR_DATA_TABLE_ENTRY.DllBase (kernel32)
00420032  MOV EBP,DWORD PTR DS:[EAX+18]
00420035  TEST EBP,EBP                  ; module load address != 0?
00420037  JNE SHORT 00420047
00420039  PUSH 0
0042003B  PUSH 0
0042003D  PUSH EDX
0042003E  PUSH 0
00420040  CALL DWORD PTR DS:[43001C]    ; user32.MessageBoxA
00420046  RETN
00420047  XOR EAX,EAX
00420049  LODS BYTE PTR DS:[ESI]        ; symbol (imports) range counter
0042004A  XCHG EAX,ECX                  
0042004B  PUSHAD                        
0042004C  MOV EAX,DWORD PTR SS:[EBP+3C] ; EBP = module address, DOS->e_lfanew
0042004F  ADD EAX,EBP                   ; VA PE header
00420051  MOV EDX,DWORD PTR DS:[EAX+78] ; RVA export directory
00420054  ADD EDX,EBP                   ;
00420056  MOV ECX,DWORD PTR DS:[EDX+18] ; Exports: NumberOfNames
00420059  MOV EAX,DWORD PTR DS:[EDX+20] ; Exports: AddressOfNames
0042005C  ADD EAX,EBP                   ;
0042005E  MOV ESI,DWORD PTR DS:[ECX*4+EAX-4] ; RVA sym_name[entry]
00420062  ADD ESI,EBP                   ; VA sym_name[entry]
00420064  XOR EDI,EDI
00420066  ROL EDI,6                     ; sym_hash #
00420069  XOR EAX,EAX
0042006B  LODS BYTE PTR DS:[ESI]        ; sym_name[entry][char]
0042006C  XOR EDI,EAX                   ; sym_hash #
0042006E  DEC EAX                       
0042006F  JGE SHORT 00420066            ; reached null terminator?
00420071  CMP EDI,DWORD PTR DS:[EBX]    ; hashed function name match?
00420073  LOOPNZ SHORT 00420059         ; entry-- ; next (top-down) entry
00420075  MOV EAX,DWORD PTR DS:[EDX+24] ; Exports: AddressOfNameOrdinals
00420078  ADD EAX,EBP
0042007A  MOV CX,WORD PTR DS:[ECX*2+EAX]; ECX = number of function
0042007E  MOV EAX,DWORD PTR DS:[EDX+1C] ; Exports: AddressOfFunctions
00420081  ADD EAX,EBP
00420083  LEA EAX,[ECX*4+EAX]           ; RVA API entry
00420086  MOV DWORD PTR SS:[ESP+14],EAX
0042008A  POPAD
0042008B  MOV EAX,DWORD PTR DS:[EDX]
0042008D  ADD EAX,EBP                   ; VA API entry
0042008F  ADD EDX,4                     ; entry++
00420092  STOS DWORD PTR ES:[EDI]       ; store entry in custom IAT
00420093  DEC BYTE PTR DS:[ESI]         ; range_entry counter--
00420095  JNE SHORT 0042008B
00420097  INC ESI                       ; next range index
00420098  ADD EBX,4                     ; next hash
0042009B  LOOP SHORT 0042004B           ; range counter--, next iteration
0042009D  PUSH ESI
0042009E  CALL DWORD PTR DS:[430008]    ; kernel32.LoadLibraryA
004200A4  XCHG EAX,EBP                  ; EBP = module load address
004200A5  MOV EDX,ESI
004200A7  LODS BYTE PTR DS:[ESI]
004200A8  DEC AL
004200AA  JNS SHORT 004200A7
004200AC  INC AL
004200AE  JE SHORT 00420035
--- snip ---

First, let me recap the export directory for you since the relation of the
arrays/tables are important here. It points to 3 arrays/tables, containing
RVAs:

* AddressOfFunctions
* AddressOfNames
* AddressOfNameOrdinals

Some general properties of these arrays:

* 'Names' and 'NameOrdinals' arrays = same length = NumberOfNames

* 'Names' and 'NameOrdinals' arrays can be shorter than 'Functions' array if
functions are exported purely by ordinal. In extreme case they can be even
empty (all exports ordinals only)

* The Ordinal of each exported function is the index in 'Functions' array.
Important: starting from 1 (!)

* The relation between name and function address is done via 'NameOrdinals'
array. Each entry in 'NameOrdinals' is an index into 'Functions' array.
Important: starting from 0 (!)

---

As indicated in the preamble, the imports resolver uses a clever way to
minimize the amount of information needed to resolve/lookup function addresses.

After loading each dll, it traverses the export directory 'AddressOfNames'
array from top-down (last entry is processed first) and hashes each API
function name.
The hash algorithm from the disassembly can be written in C code like this:

--- snip ---
int result = 0x0;

while (*str) {
    result = _rotl(result, 6);
    result ^= *str++;
}
--- snip ---

Every bit of the input has to affect the result (character set of the string).
Basically the current result is rotated by a number of bits and then the
current result is XORed with the current string byte. Repeat until the end of
the string.

The compressor has two internal tables for each dll:

* pre-computed 32-bit hashes for API function names
* number of consecutive functions to import (range)

It compares the hashed value of the API function name to a pre-computed 32-bit
hash from above table. When it finds a match, it uses the 'NameOrdinals' array
for further lookup. The association of name and function address is done in
'NameOrdinals' array. Each entry in 'NameOrdinals' is an index into 'Functions'
array (starting from 0). The position of the entry in 'NameOrdinals' denotes
the position of name in 'Names' array with the name of the function.

In the case of 'opengl32.dll' only two ranges are used.

range #1: glBindTexture + 241 (0xf1) consecutive functions
range #2: glTexParameteri + 46 (0x2e) consecutive functions

To illustrate further, a dump of the resolver's custom IAT after processing:

--- snip ---
Address     Value        Comments
00430000     7B42F93C    ; kernel32.CreateThread
00430004     7B45010C    ; kernel32.ExitProcess
00430008     7B430DF0    ; kernel32.LoadLibraryA
0043000C     7E8910A0    ; user32.ChangeDisplaySettingsA
00430010     7E89A250    ; user32.CreateWindowExA
00430014     7E84FE90    ; user32.GetAsyncKeyState
00430018     7E87B320    ; user32.GetDC
0043001C     7E875C60    ; user32.MessageBoxA
00430020     7E8717E0    ; user32.PeekMessageA
00430024     7E82A7E0    ; user32.ShowCursor
00430028     7E597AA0    ; gdi32.ChoosePixelFormat
0043002C     7E597C40    ; gdi32.SetPixelFormat
00430030     7E597CD0    ; gdi32.SwapBuffers

; opengl32 imports range #1
00430034     7A8AD8D0    ; opengl32.glBindTexture
00430038     7A8AD940    ; opengl32.glBitmap
...
004303C4     7A8B48F0    ; opengl32.glRasterPos4i
004303C8     7A8B4970    ; opengl32.glRasterPos4iv
004303CC     7A8B49E0    ; opengl32.glRasterPos4s
004303D0     7A8B4A70    ; opengl32.glRasterPos4sv
004303D4     7A8B4AE0    ; opengl32.glReadBuffer
004303D8     7A8B4B50    ; opengl32.glReadPixels
004303DC     7A8B4C00    ; opengl32.glRectd
004303E0     7A8B4CB0    ; opengl32.glRectdv
004303E4     7A8B4D20    ; opengl32.glRectf
004303E8     7A8B4DE0    ; opengl32.glRectfv
004303EC     7A8B4E50    ; opengl32.glRecti
004303F0     7A8B4ED0    ; opengl32.glRectiv
004303F4     7A8B4F40    ; opengl32.glRects

; opengl32 imports range #2
004303F8     7A8B6DB0    ; opengl32.glTexParameteri
004303FC     7A8B6E30    ; opengl32.glTexParameteriv
00430400     7A8B6EB0    ; opengl32.glTexSubImage1D
00430404     7A8B6F60    ; opengl32.glTexSubImage2D
00430408     7A8B7020    ; opengl32.glTranslated
0043040C     7A8B70C0    ; opengl32.glTranslatef
00430410     7A8B7160    ; opengl32.glVertex2d
...
004304A4     7A8BB980    ; opengl32.wglGetProcAddress
004304A8     7A8B98A0    ; opengl32.wglMakeCurrent
004304AC     7A8BB2A0    ; opengl32.wglRealizeLayerPalette
004304B0     00000000
004304B4     00000000
--- snip --- 

Since import-by-ordinal mechanics are not used for resolving as explained
above, misordered or even missing functions are a problem. The custom imports
resolver relies on the fact that all needed API functions appear in a specific
order in these ranges.

To investigate the discrepancy you basically use Wine's 'winedump' tool to dump
exports from Wine builtin and native opengl32. Grep out each range, by using
the start function name (see range #1 and range #2) and the count (-A argument
in grep) to generate a range "diff".

aaaaand the offender is: 'wglGetDefaultProcAddress'

It seems AJ's commit
https://source.winehq.org/git/wine.git/commitdiff/17dffaac7d5a8fc595775d456bb85e343d5aa195
("opengl32: Get WGL function definitions from the XML files.") in October 2017
removed it.

--- snip ---
-@ stub    wglGetDefaultProcAddress
--- snip --- 

BTW. the demo still crashes if you re-add the stub again. That's bug 43638
("Yermom demoscene demo crashes on start up") for you ;-)

Regards

-- 
Do not reply to this email, post in Bugzilla using the
above URL to reply.
You are receiving this mail because:
You are watching all bug changes.



More information about the wine-bugs mailing list