Wine은 어떻게 linux에서 windows app을 실행하나?

Wine은 어떻게 linux에서 windows app을 실행하나?

지난 포스트 에서 windows 와 linux의 차이를 알았다.
그렇다면, Wine 은 그 간극을 매꾸기 위해 어떻게 구현 하였는지 알아보자.

시작하기 전에 잡지식 얘기하자면,
WINE은 Wine Is Not Emulator의 약자이다. (그런데 내 생각에는 emulator가 맞는 것 같다.;;)

1. Wine 의 구조 - Builtin / Native Dlls

wine은 무엇을 구현했는가?
첫째로 PE binary 를 실행시켜줄 Loader(&linker) 를 구현했다.
(실행을 시키는 상황은 차차 설명하기로 하고...)
둘때로 windows의 dll들을 죄다! 직접! 구현했다.

실행이 되었을때의 구조는 아래와 같다.

dlls

chrome.exe가 실제로 저렇게 물고있지는 않고, 예시를 그냥 들어본 것이다.
chrome.exe는 사용자가 작성한 프로그램이고, 역시 사용자가 작성한 chrome_elf.dll을 물고있다.
이 사용자 프로그램들은 kernel32, user32, url 등의 windows dll들을 물고 있다.
여기서 windows dll들은 원래 존재하는 것들이다. MS가 만들어서 제공하는 것.
linux에는 이런 친구들이 없다. 이것들을 죄다 구현한 것이다.

빨간색에 해당하는 사용자가 작성한 dll이 native dll,
파란핵에 해당하는 windows가 제공하는 dll이 builtin dll 이다.

chrome을 코딩하는 사용자 관점에서 보자면, user32의 CreateWindow를 호출하여 창을 띄우고, kernel32의 CreateThread를 호출하여 thread를 생성하는 등의 코딩을 한다.
이를 linux에서 돌리려면, CreateThread 대신에 pthread_create 등의 함수를 사용하여 다시 만들어야 하지만, 이를 wine이 대신 해주는 것이다.

windows에는 정말 무수한 dll과 함수들이 있다. 이를 정말 거의 다 구현했다.
wine/dlls/ 를 한번 봐보기를.


2. Builtin Dlls의 구조 - PE + ELF

Builtin Dll들은 사실 elf 이다.
kernel32.dll.so 처럼 특이한 이름으로 생성이 된다.
PE 를 모사하였는데, 어떻게 되어있냐하면...... PE header를 빌드타임에 생성하였다.

kernel32_bin

kernel32.dll.so를 그냥 한번 vim 으로 열어본 것인데, 저렇게 함수이름들이 박혀있다.
어디서 많이 본 모양이 아닌가? - EAT이다.
PE Header에 들어가야할 내용들이 모두 다 만들어져 있고, 실제로 memory에 올라갈 때, 적절한 위치에 복사된다. 그리고, 마치 그곳이 kernel32.dll의 image base 인 것처럼 셋팅이 된다!
분명 같은 파일인데, 원래 elf 파일인데, PE 로 취급하면 PE 인척 할 수 있도록 말이다.

dlls/kernel32/kernel32.dll.spec 파일을 보면 아래와 같이 각 함수들의 스펙이 적혀있다. 이 정보를 보고 PE header를 생성한다.
친절하게도 미구현 함수들은 stub이라고 표시되어 있다.
이런애들 구현하면 wine에 기여할 수 있겟지.

@ stub -i386 AllocLSCallback
@ stdcall -i386 -private AllocSLCallback(ptr ptr) krnl386.exe16.AllocSLCallback
@ stdcall -import AllocateUserPhysicalPages(long ptr ptr)
@ stdcall -import AllocateUserPhysicalPagesNuma(long ptr ptr long)
@ stdcall ApplicationRecoveryFinished(long)
@ stdcall ApplicationRecoveryInProgress(ptr)
@ stdcall -import AreFileApisANSI()
@ stdcall AssignProcessToJobObject(ptr ptr)
@ stdcall -import AttachConsole(long)
@ stdcall BackupRead(ptr ptr long ptr long long ptr)
@ stdcall BackupSeek(ptr long long ptr ptr ptr)
@ stdcall BackupWrite(ptr ptr long ptr long long ptr)
@ stub BaseAttachCompleteThunk
@ stub BaseCheckAppcompatCache

3. Binfmt_misc 등록

프로그램 실행의 처음부터 보자.
기본적으로 Linux는 PE 의 실행을 지원하지 않는다.

Wine을 설치하면? 달라진다.
Linux kernel에는 binfmt_misc 라는 모듈이 있어서, 여기에 새로운 형식들을 등록할 수 있다.
Debian 계열에서는 update-binfmts 라는 도구를 통해 손쉽게 추가/삭제/비활성 등이 가능하다.

wine-binfmts

python 들도, jar 도 이와 같은 방법을 통해 바로 실행시킬 수 있도록 설치되어 있다.
wine으로 등록된 것을 보면 MZ 가 magic 으로 추가되어있다.
Exec 으로 pe binary가 들어오면(./a.exe), wine ./a.exe 로 실행하도록 변경하는 것이다.
이 binfmts가 등록이 안되어있다면, pe exe파일을 인식할 수 없기 때문에 아래와 같이 오류가 발생한다.

exec_error


4. Wine binary & preloader

그럼 wine binary는 무엇인고.
wine 을 빌드하면 루트 디렉토리에 wine이라는 파일이 있다.

lrwxrwxrwx 1 jonhpark jonhpark 17 2월 16 20:17 wine -> tools/winewrapper
그러나, 이는 tools/winewrapper를 가리키고 있고, 이놈은 또 shell 스크립트이다.

결국에는 loader/wine 을 실행시키는데,
loader/main.c를 보면 사실은 loader/wine-preloader loader/wine a.exe 가 실행되도록 한다. (참 쓸데없이 복잡하기도하다.)
여기서 부터는 다들 elf 포맷의 실행 파일들이다.

preloader

Preloader 에서는 앞서 얘기한 memory layout 을 첫째로 잡아준다.
2G - 3G 를 anon 으로 mmap 하여 아무도 못쓰게.

특이점은, preloader는 libc조차 물고 있지 않다.

ldd_winepreloader

기본적으로 elf 프로그램들은 libc를 물고 있다.
Loading, linking, + 기본적인 기능(fork, printf 와 같은 함수들) 을 사용하기 위함이다.
우리가 std c library 라고 알고 있는 것들이 다 libc에 구현되어 있다.
추가로, system call 이라고 생각하고 사용하는 것들도 여기에 구현된 Wrapper를 사용하는 것이다.
(원래 system call 은 그냥 asm 이어야 하니까.)

Preloader는 메모리를 선점해야하기 때문에, 아무도 없는 백지 상태이기를 원한다.
그래서 libc조차 올라오는 것을 원치 않기 때문에 빌드할때 libc를 빼고 빌드한다.
깔끔한 상태에서 원하는 곳들을 mmap 할 수 있다.
문제는... mmap 조차 그냥 편하게 호출 할 수 없다. Asm으로 syscall 을 날려주어야 한다.
loader/preloader.c 를 보면 아래와 같이 mmap을 구현해 두었다...
syscall을 호출하기위해 int 0x80을 실행한다.

mmap_wld

Preloader는 메모리를 선점한 후, "wine" binary와 libc (ld.so)를 메모리에 올린 후,
Libc에게 제어권을 넘긴다.

preloader_code

wine binary 가 main binary이고, wine의 interp 가 libc이다.
올리는 코드 (map_so_lib)도 사실은 libc에 구현되어있는 코드를 가져온 것이다.

보통의 경우라면 kernel 이 이 두 바이너리를 올리고 넘어간 것과 같다.
Libc 는 이제 프로세스 초기화를 진행하고, "wine" 의 main을 호출하여 wine 코드가 시작된다.


5. Process 초기화

Wine 은 이제 ntdll / kernel32 를 올리며, 초기화 과정을 진행한다.
두 dll 은 windows의 libc와 같은 가장 기본적인 라이브러리이다.
모든 프로그램은 두 라이브러리를 물고 있다.
(실제로 ntdll이 메모리에 안올라와 있다면 그냥 죽어버리는 프로그램들도 있다;;;)

Ntdll을 올리고 거기에 있는 초기화 함수인 wine_process_init 호출한다.

load_ntdll

여기서는 첫째로 thread를 초기화 한다.
PEB와 TEB를 생성한다. 앞선 포스트에서 언급했듯이 두 구조체는 언제나 항상 존재하는 global한 구조체이고, gs register를 통해 접근이 가능하다.

extern void set_full_cpu_context( const CONTEXT *context );
__ASM_GLOBAL_FUNC( set_full_cpu_context,
                   "movl 4(%esp),%ecx\n\t"
                   "movw 0x8c(%ecx),%gs\n\t"  /* SegGs */
                   "movw 0x90(%ecx),%fs\n\t"  /* SegFs */
...

그래서 여기서 정보를 위처럼 직접 gs register에 주소값을 쑤셔넣는다.

그리고,** wine server를 셋팅한다.**
wine 프로젝트에는 wineserver라는 binary가 있는데, 데몬으로 떠있다.
windows의 커널이 하는 일을 처리하는 프로그램이다.
여러 프로세스간의 통신이 필요한 일들을 처리하기 때문에, 데몬으로 존재한다.
wine server를 실행시켜서 socket으로 연결한다.

그리고는, gloabl option을 로드한다.
registry를 읽어서 heap이나 critical section등 low-level 셋팅들을 한다.

Kernel32을 올리고 wine_kernel_init을 호출한다.

여기서는 한는일이 꽤나 많은데...
locale 셋팅하고, current directory 셋팅하고, process name 셋팅하고, cmd line 셋팅하고, window title 셋팅하고, version 정보 셋팅하고, 첫 ldr module (main exe)만들고, 등등...
그 이후에, wine server에 생성되었음을 알린다. wine server는 프로세스가 생성되었음을 기록한다.

이제 필요한 것들을 했으니, main exe와 dependent한 dll 들을 다 로딩한다.
attach를 다 해주고, main exe 의 entry point를 호출해준다.
attach 란 각 pe module의 초기화 과정들을 불러주는 것이다.
dll들은 dllMain 이라는 함수를 모두다 가지고 있고, 이를 호출한다.
dll 제작자들은 여기에 자신들이 원하는 초기화 과정을 구현하게 되어있다.
gcc의 constructor와 같은 것이다.


6. Linking

Wine이 구현한 포인트들. 프로세스의 실행 과정까지 알았다.
이번에는 Linking 과정이다.
PE와 ELF는 linking 스펙이 다르기 때문에 여기도 맞춰주어야 한다.

사실 linking 의 본질은 복잡하지 않다.
그냥 함수을 호출하는 코드에게 호출해야할 주소를 찾아서 알려주기만 하면 끝이다.

예를 들어, 내가 a.exe에서 CreateThread 라는 함수를 호출한다고 가정해보자.
메모리에서 CreateThread 주소를 알아와주기만 하면 linking은 끝이다.
PE에서는 이를 어떻게 아느냐, a.exe 에는 CreateThread가 kernel32.dll 에 속한 함수라는 것이 명시되어 있다.
kernel32.dll의 EAT에 가면 CreateThread 의 주소가 어디인지 명시되어 있다.
주소를 읽어다가 a.exe의 IAT에 write해 주기만 하면된다.
a.exe의 호출코드는 IAT에 가서 주소를 read 한 후, call instruction 을 실행한다.

여기서 wine의 특이한 점은 builtin dll끼리의 함수 호출도 IAT를 통해 이루어 진다는 것이다.
다시 예를 들어, user32.dll.so 에서 CreateThread를 호출한다고 가정해보자.
사실 user32.dll.so도 kernel32.dll.so도 elf이기 때문에 그냥 elf의 방식대로 plt/got 를 사용하여 호출하면 된다.
그러나, wine에서는 EAT/IAT를 사용하여 호출한다. 이렇게 만들어지도록 build 시스템을 직접 제작했다.

장점은? 함수이름이 겹쳐도 된다는 것.
EAT/IAT 방식에서는 함수가 어떤 dll에 속한 함수인지 까지도 명시하기 떄문이다.
plt/got에서는 함수이름이 겹치면 모호해서 올바른 함수를 찾아줄 수 없다.


요약

Wine은 개쩐다.
구현한 코드의 양도 방대하고.
빌드 시스템부터 loader, linker, kernel 기능 등. 구현한 내용들도 빡센 것들이 많다.

심지어 특정 앱에서, 특정 주소에, 특정 값이 박혀있어야 한다는 것도 리버싱으로 알아내서 박아둔 내용들도 있다. 그냥 미쳤다. 찬양하라.