"응용 프로그램을 제대로 시작하지 못했습니다(0xc0000022). 응용 프로그램을 닫으려면 [확인]을 클릭하십시오." 확인을 누르면 창이 닫히고 프로그램도 같이 사라진다. 다시 실행해도 똑같다. 문구만 봐서는 무엇이 잘못됐는지 아무 단서가 없지만, 괄호 안의 숫자가 거의 모든 걸 말해 준다. 이 연재는 이런 숫자를 하나씩 풀어 보는 글이고, 첫 번째가 0xc0000022 다.

0xc0000022 는 "액세스 거부"다

0xc0000022 는 Windows 커널이 쓰는 상태 코드(NTSTATUS)의 하나로, 이름은 STATUS_ACCESS_DENIED 다. ntdll 의 메시지 표에서 이 코드를 꺼내 보면 "{액세스 거부} 프로세스가 개체에 액세스를 요구했지만 액세스 권한이 없습니다"가 나온다. 일반 프로그램이 쓰는 Win32 오류 번호로 바꾸면 5, 우리가 흔히 보는 "액세스가 거부되었습니다"와 같은 뜻이다.

앞의 0xc 는 오류라는 표시이고, 뒤의 0022 가 실제 내용이다. 같은 창에 0xc000007b(비트가 다른 DLL)가 들어가기도 하는데, 그건 다음 편에서 다룬다.

중요한 건 이 창이 언제 뜨느냐다. "제대로 시작하지 못했다"는 말 그대로, 실행 파일은 이미 메모리에 올라왔고 Windows 가 그 프로그램이 쓰는 DLL 들을 이어 붙이는 도중에 실패했다는 뜻이다.

사본으로 같은 창을 띄워 보았다

정말 그런지 직접 확인했다. 시스템 파일은 건드리지 않고 %TEMP% 아래 실험용 폴더에 사본만 만들었다. 재료는 Windows 에 들어 있는 64비트 명령줄 도구 where.exe 와, 이 프로그램이 시작할 때 불러오는 version.dll 이다. 둘을 같은 폴더에 복사하면 Windows 는 프로그램 폴더를 먼저 찾기 때문에 사본 옆의 version.dll 을 쓴다.

사본은 처음엔 정상으로 돌았다. where.exe cmd 를 실행하니 C:\Windows\System32\cmd.exe 가 출력되고 종료 코드는 0 이었다. 그다음 DLL 사본 하나에만 Everyone 의 읽기·실행을 막는 거부 항목을 넣었다.

icacls "$env:TEMP\실험\version.dll" /deny "*S-1-1-0:(RX)"

*S-1-1-0 은 Everyone 그룹을 언어와 상관없이 가리키는 이름이다. 이 상태에서 사본 where.exe 를 실행하자 바로 그 창이 떴다. 이벤트 뷰어의 시스템 로그에도 "Application Popup" 이벤트(ID 26)로 같은 문장이 그대로 남았다.

where.exe - 응용 프로그램 오류
응용 프로그램을 제대로 시작하지 못했습니다(0xc0000022). 응용 프로그램을 닫으려면 [확인]을 클릭하십시오.

오류 창을 띄우지 않게 하고 다시 실행하면 종료 코드가 0xc0000022 로 돌아왔다. 거부 항목을 지우자(icacls ... /remove:d "*S-1-1-0") 다시 정상으로 실행됐다.

비교를 위해 이번에는 DLL 이 아니라 실행 파일 사본 자체의 실행 권한을 막아 보았다. 결과가 달랐다. 0xc0000022 창은 뜨지 않고, 명령 프롬프트에 "액세스가 거부되었습니다"만 나왔다. 실행 파일을 열지 못하면 프로세스가 아예 만들어지지 않으니 "시작하지 못했다"는 창까지 갈 일이 없는 것이다. 바꿔 말하면 0xc0000022 창이 떴다는 건 실행 파일은 읽혔고, 그 프로그램이 함께 불러오는 DLL 중 어느 것인가가 막혔다는 뜻이다. 범인을 찾을 곳이 실행 파일 옆 DLL 들로 좁혀진다.

어느 파일이 막혔는지 찾기

권한은 icacls 로 읽는다. 읽기만 하는 명령이라 일반 권한으로 실행해도 된다. 정상 상태가 어떤지 먼저 보면 비교가 쉽다. 이 PC 에서 icacls C:\Windows\System32\version.dll 을 실행하면 TrustedInstaller 가 모든 권한(F), Administrators·SYSTEM·Users 와 앱 패키지 그룹이 읽기·실행(RX)을 갖고 있었다. C:\Program Files 도 Users 에게 읽기·실행을 주고 그 아래로 상속((OI)(CI))하는 구조였다. 정상 파일에는 (DENY) 가 없고, 보통 사용자 그룹이 최소한 RX 를 갖는다.

icacls "C:\Program Files\프로그램폴더\문제의.dll"

결과에 (DENY) 가 보이거나, Users·Everyone 같은 일반 사용자 그룹이 아예 빠져 있고 모르는 계정 번호(S-1-5-21-...)만 남아 있다면 그 파일이 의심스럽다. 모르는 계정 번호는 다른 PC 에서 만들어진 파일의 권한이 그대로 따라온 흔적인 경우가 많다.

DLL 이 수십 개라면 하나씩 보기 어렵다. 지금 계정으로 실제로 열어 보고 실패하는 파일만 골라내는 편이 빠르다.

Get-ChildItem 'C:\Program Files\프로그램폴더' -Recurse -Include *.exe, *.dll |
    ForEach-Object {
        $f = $_.FullName
        try { [IO.File]::OpenRead($f).Close() } catch { "읽을 수 없음: $f" }
    }

실험 폴더에서 돌려 보니 거부 항목을 넣은 version.dll 하나만 정확히 걸렸다. 같은 방법으로 이 PC 의 Program Files 두 폴더 전체를 훑었을 때는 실행 파일과 DLL NNN개 가운데 읽을 수 없는 파일이 MMM개였다.

왜 권한이 틀어지나, 그리고 고치는 순서

정상적으로 설치한 프로그램의 권한은 Program Files 의 상속을 그대로 받으니 틀어질 일이 드물다. 흔히 알려진 원인은 다른 PC 나 백업 디스크에서 프로그램 폴더를 통째로 복사·복원한 경우, 권한을 "정리"해 준다는 도구를 돌린 경우, 보안 프로그램이 특정 DLL 의 접근을 막은 경우다. 실험에서 본 것처럼 DLL 하나만 막혀도 같은 창이 뜬다.

그래서 순서는 이렇게 잡는다. 먼저 보안 프로그램 기록부터 본다. Windows 보안을 쓴다면 "바이러스 및 위협 방지 > 보호 기록"에서 그 프로그램 폴더의 파일이 차단·격리됐는지 확인한다. 기록이 있다면 그 파일이 정말 정상인지부터 따져야 한다. 확신이 없으면 허용하지 말고 공식 사이트의 새 설치 파일로 다시 설치한다. 실시간 보호를 끄고 실행하는 건 원인을 확인하는 방법이 아니라 위험을 하나 더 만드는 일이다.

보안 프로그램 기록이 깨끗하다면 권한 문제다. 가장 깔끔한 해결은 프로그램을 제거하고 다시 설치하는 것이다. 설치 프로그램이 파일을 새로 만들면 Program Files 의 정상 권한을 다시 상속받는다. 재설치가 어렵다면 그 프로그램 폴더 하나만 상속 권한으로 되돌리는 방법이 있다. 관리자 권한 명령 프롬프트에서 실행한다.

icacls "C:\Program Files\프로그램폴더" /reset /t /c

/reset 은 명시적으로 붙은 권한을 지우고 상위 폴더의 상속 권한으로 되돌린다. 이 명령을 C:\Windows 나 C:\Program Files 전체, 드라이브 루트에 쓰면 안 된다. 시스템 폴더에는 일부러 상속과 다르게 잡아 둔 권한이 많아서, 한 번에 되돌리면 Windows 업데이트나 다른 프로그램이 깨질 수 있다. "모든 사용자에게 모든 권한"을 주는 것도 같은 이유로 피한다.

Windows 기본 프로그램(메모장, 계산기, 설정 같은 것)에서 0xc0000022 가 난다면 System32 쪽 파일 문제일 가능성이 크다. 이때는 권한을 직접 만지지 말고 관리자 권한 명령 프롬프트에서 Windows 파일 검사를 돌린다.

sfc /scannow
DISM /Online /Cleanup-Image /RestoreHealth

sfc 는 Windows 구성 요소 파일이 원래 것과 다르면 원본으로 되돌리고, DISM 은 sfc 가 참고하는 원본 저장소를 먼저 고친다. sfc 가 "복구하지 못했다"고 하면 DISM 을 먼저 실행한 뒤 sfc 를 한 번 더 돌린다.

다음 편은 같은 창에 숫자만 다른 0xc000007b 다. 이번엔 권한이 아니라 64비트 프로그램이 32비트 DLL 을 불러올 때 생기는 문제이고, DLL 파일 머리의 두 바이트로 비트를 읽어 내는 법을 다룬다.