앞 편에서 다룬 0xc0000022 와 똑같은 창이 숫자만 바꿔서 뜬다. "응용 프로그램을 제대로 시작하지 못했습니다(0xc000007b)." 오래된 게임이나 사내 프로그램을 새 PC 에 옮겼을 때 자주 보는 코드다. 문구는 같아도 원인은 전혀 다르다. 이번에는 권한이 아니라 비트다.
0xc000007b 는 "실행할 수 없는 이미지 형식"
0xc000007b 는 NTSTATUS 값 STATUS_INVALID_IMAGE_FORMAT 이다. ntdll 의 메시지 표에서 이 코드를 꺼내 보면 "{잘못된 이미지} ...에 오류가 있거나 Windows에서 실행할 수 없는 이미지입니다"라는 문장이 나오고, Win32 오류 번호로 바꾸면 193(ERROR_BAD_EXE_FORMAT)이 된다. 193 은 "올바른 Win32 응용 프로그램이 아닙니다"라는 그 오류다.
여기서 "이미지"는 실행 파일과 DLL 을 함께 부르는 말이다. 파일이 깨졌을 수도 있지만, 실제로 가장 흔한 경우는 파일은 멀쩡한데 프로그램과 DLL 의 비트가 다른 것이다. 64비트 프로세스는 64비트 DLL 만, 32비트 프로세스는 32비트 DLL 만 자기 안에 올릴 수 있다. 섞어서 불러올 방법이 없다.
64비트 프로그램 옆에 32비트 DLL 을 두어 보았다
직접 띄워 보려고 %TEMP% 아래 실험용 폴더에 where.exe 사본을 만들었다. 시스템 파일은 건드리지 않고 사본만 쓴다. 이 64비트 프로그램은 시작할 때 VERSION.dll 을 불러온다. 그 옆에 SysWOW64 에 있는 32비트 version.dll 사본을 두고 실행했다.
결과는 의외로 정상 실행이었다. 종료 코드 0, 출력도 제대로 나왔다. 앞 편에서 같은 자리의 DLL 권한을 막았을 때는 바로 오류가 났으니, 프로그램 폴더를 먼저 찾는 것은 맞다. 다만 이 PC 의 로더는 비트가 맞지 않는 DLL 을 만나자 그것을 건너뛰고 다음 검색 위치인 System32 의 64비트 version.dll 을 쓴 것으로 보인다.
그래서 이번에는 대신 쓸 DLL 이 없는 상황을 만들었다. 사본 실행 파일의 가져오기 목록에서 VERSION.dll 이라는 글자를 VERSIOX.dll 로 한 글자 바꾸고, 32비트 version.dll 사본의 이름도 VERSIOX.dll 로 바꿨다. Windows 어디에도 64비트 VERSIOX.dll 은 없으니 로더가 고를 수 있는 건 32비트 파일 하나뿐이다. 실행하자 이 창이 떴다.
wherex.exe - 응용 프로그램 오류
응용 프로그램을 제대로 시작하지 못했습니다(0xc000007b). 응용 프로그램을 닫으려면 [확인]을 클릭하십시오.
종료 코드도 0xc000007b 였다. 같은 32비트 DLL 을 64비트 PowerShell 에서 LoadLibraryEx 로 직접 불러 보면 이번에는 창 없이 오류 193 이 돌아온다. 시작할 때 묶여 있는 DLL 이 틀리면 0xc000007b 창, 실행 중에 불러오는 DLL 이 틀리면 193 이라는 차이다.
이 실험에서 실제 상황을 짐작할 수 있다. 0xc000007b 는 프로그램 전용 DLL 처럼 대신 쓸 파일이 다른 곳에 없을 때, 또는 System32 에 엉뚱한 비트의 파일이 들어가 있을 때 잘 생긴다. 게임 폴더에 다른 버전의 DLL 을 덮어쓴 경우, 인터넷에서 받은 32비트 DLL 을 System32 에 넣은 경우가 대표적이다.
PE 헤더의 두 바이트로 비트를 읽는다
DLL 의 비트는 파일 앞부분에 적혀 있다. Windows 실행 파일(PE 형식)은 0x3C 위치에 PE 헤더가 어디서 시작하는지 적어 두고, 그 위치에서 PE\0\0 네 바이트 다음 두 바이트가 Machine 값이다. 32비트는 0x14c, 64비트는 0x8664, ARM64 는 0xaa64 다. 이 PC 의 version.dll 두 개를 열어 보면 이렇다.
| 파일 | PE 헤더 위치 | 헤더 바이트 | Machine |
|---|---|---|---|
| System32\version.dll | 0xF0 | 50 45 00 00 64 86 | 0x8664 (64비트) |
| SysWOW64\version.dll | 0xF8 | 50 45 00 00 4C 01 | 0x14c (32비트) |
값이 거꾸로 보이는 건 바이트를 작은 자리부터 적는 방식이라서다. 64 86 이 0x8664, 4C 01 이 0x014c 다. PowerShell 에서는 이렇게 읽는다. 파일 앞 4KB 만 읽으니 큰 DLL 도 금방 끝난다.
function Get-PeMachine([string]$Path) {
$fs = [IO.File]::OpenRead($Path)
try { $b = New-Object byte[] 4096; [void]$fs.Read($b, 0, $b.Length) }
finally { $fs.Close() }
$pe = [BitConverter]::ToInt32($b, 0x3C)
switch ([BitConverter]::ToUInt16($b, $pe + 4)) {
0x14c { 'x86 (32-bit)' }
0x8664 { 'x64 (64-bit)' }
0xaa64 { 'ARM64' }
default { '0x{0:x}' -f $_ }
}
}
Get-ChildItem 'C:\Program Files\프로그램폴더' -Recurse -Include *.exe, *.dll |
Select-Object Name, @{n='Bit'; e={ Get-PeMachine $_.FullName }}
폴더 이름만 문제의 프로그램 것으로 바꾸면 된다. 실행 파일은 64비트인데 그 옆 DLL 하나만 32비트라면 그 파일이 범인이다. 반대로 실행 파일이 32비트인데 64비트 DLL 이 섞여 있어도 같은 오류가 난다.
System32 는 64비트, SysWOW64 는 32비트
DLL 을 확인하다 보면 폴더 이름에서 한 번 헷갈린다. 64비트 Windows 에서 C:\Windows\System32 에는 64비트 파일이, C:\Windows\SysWOW64 에는 32비트 파일이 들어 있다. 이름과 반대다. 이 PC 에서 세어 보니 System32 에 DLL 이 3,692개, SysWOW64 에 2,391개 있었다. 오래된 프로그램들이 System32 라는 경로를 고정으로 써 왔기 때문에 64비트 Windows 는 그 이름을 64비트 쪽에 그대로 두고, 32비트용을 WOW64(Windows 32-bit on Windows 64-bit)라는 새 폴더로 뺀 것으로 알려져 있다.
32비트 프로그램이 System32 를 열면 Windows 가 몰래 SysWOW64 로 돌려 준다. 32비트 PowerShell 에서 C:\Windows\System32\version.dll 의 Machine 을 읽으면 0x14c 가 나왔다. 같은 32비트 PowerShell 에서 진짜 64비트 쪽을 보려면 C:\Windows\Sysnative\version.dll 이라는 가상 경로를 써야 했고, 그때 0x8664 가 나왔다. 32비트 설치 프로그램이나 압축 도구로 System32 에 파일을 넣으면 실제로는 SysWOW64 에 들어가는 일이 생기는 이유다.
그래서 "DLL 을 System32 에 넣으라"는 오래된 안내를 64비트 PC 에서 따라 하면 0xc000007b 가 생기기 쉽다.
고치는 순서
먼저 범인을 좁힌다. 위 명령으로 프로그램 폴더의 실행 파일과 DLL 비트를 비교하고, 다른 비트의 DLL 이 있다면 직접 받아 넣었거나 다른 버전에서 복사해 온 파일인지 떠올려 본다. 이럴 때는 프로그램을 지우고 공식 설치 파일로 다시 설치하는 쪽이 DLL 을 한 개씩 바꿔 끼우는 것보다 깨끗하다.
프로그램 폴더가 깨끗한데도 오류가 나면 런타임 쪽을 본다. 게임이나 업무 프로그램이 쓰는 Visual C++ 런타임, DirectX, .NET 같은 구성 요소는 32비트용과 64비트용이 따로 있어서, 한쪽만 깔려 있거나 한쪽이 손상되면 이 코드가 나오는 경우가 많은 것으로 알려져 있다. Visual C++ 런타임은 다음 편에서 자세히 다룬다.
System32 에 엉뚱한 비트의 파일이 들어갔다고 의심되면 직접 지우거나 바꾸지 말고 Windows 의 시스템 파일 검사를 쓴다. 관리자 권한 명령 프롬프트에서 실행해야 한다.
sfc /scannow
DISM /Online /Cleanup-Image /RestoreHealth
sfc 는 Windows 구성 요소 파일이 원래 것과 다르면 되돌리고, DISM 은 sfc 가 쓰는 원본 저장소를 먼저 고친다. Windows 가 원래 갖고 있지 않던 DLL 은 sfc 대상이 아닌 것으로 보이니, 그런 파일은 그 DLL 을 쓰는 프로그램이나 런타임을 다시 설치해 정리한다. DLL 다운로드 사이트에서 같은 이름을 받아 덮어쓰는 건 비트가 맞는다는 보장이 없어 문제를 하나 더 만들 뿐이다.
다음 편은 0xc000007b 의 단골 원인이기도 한 "VCRUNTIME140.dll 이(가) 없어 코드 실행을 진행할 수 없습니다" 창이다. 레지스트리에서 설치된 Visual C++ 재배포 패키지 버전을 확인하고, x86 과 x64 를 둘 다 깔아야 하는 이유를 본다.