Wine 아래에서 실행되는 Windows PE 실행 파일에서 리눅스 규약의 syscall을 직접 호출해보는 장난스러운 실험과 그 결과를 소개한다.
Jan 15 2026에 게시됨
Windows 11이 별로라서, 요즘 Wine을 가지고 놀고 있다. 주로 내가 정말 좋아하는 여러 Windows 전용 소프트웨어를 시험해 보면서 Linux에서 어떻게 동작하는지 확인하는 중이다. 하지만 동시에 Wine이 어떻게 동작하는지에 대해서도 표면적인 수준에서 이것저것 탐색해 보고 있다. 그러다 내가 공유하고 싶은 다음과 같은 재밌는 실험으로 이어졌다...
syscall에 대해 이야기해 보자. 아마 뭔지 알고 있을 것이다. 만약 모른다면, 이 블로그의 어셈블리 튜토리얼에서 관련 섹션을 읽어보길 바란다! 하지만 요약하자면, syscall은 운영체제가 애플리케이션을 위해 수행해 줄 수 있는 서비스다. 파일 열기/읽기/쓰기, 메모리 할당 같은 것들이 결국에는 전부 syscall을 통해 이루어진다.
위에 링크한 글에서, Windows와 Linux에서 애플리케이션이 syscall 명령을 사용하는 방식에 중요한 차이가 있다고 설명했었다. Linux에서는 애플리케이션 코드가 syscall 명령을 직접 사용하는 것이 기대되는 반면, Windows에서는 애플리케이션이 syscall을 직접 사용하는 것이 기대되지 않는다. 대신 WinAPI 함수를 호출해야 하며, 이 함수들이 뒤에서 OS 커널과의 직접 통신을 처리한다.
이 마지막 부분이 Wine에서 핵심이다. 대략적으로 말해 Wine은 Windows 포터블 실행 파일(PE)을 “그냥” 로드하고, 프로세스 주소 공간의 모든 것이 Windows처럼 보이게 만들며, WinAPI 함수 구현을 담은 자체 “시스템 라이브러리” 집합을 제공하면 된다(이 말 자체가 이미 엄청나게 ‘말은 쉽다’ 수준이고, 지금도 디테일을 많이 생략하고 있다).
Wine 아래에서 실행되는 Windows 프로그램의 코드는 다른 어떤 일반 프로세스와 마찬가지로 실행된다. 어떤 종류의 “가상화”도 없고, WinAPI가 제공하는 기능 집합 외에는 특별히 “에뮬레이션”되는 것도 없다. Linux 커널 관점에서 그 Windows 프로그램은 그냥 또 하나의 프로세스일 뿐이다.
불행히도, 모든 Windows 프로그램이 규칙을 따르지는 않는다. 사실 Windows syscall 규약은 완전히 비밀이 아니다. Microsoft는 그 규약이 안정적으로 유지된다는 보장만 하지 않을 뿐이다(애초에 거기에 의존하면 안 되니까!). 하지만 실무적으로는 많은 부분이 안정적이다. 그래서 어떤 프로그램들은 그냥 syscall을 직접 사용해 버린다. 보통은 성능상의 이유로 그렇게 한다.
그런 프로그램들은 Wine에 큰 문제가 된다. Linux에서 Windows 규약을 사용해 syscall 명령을 실행하면, 좋은 일이 일어날 리가 없다. 십중팔구 프로세스가 그냥 크래시 날 것이다. 이 문제를 우회할 가능한 방법들은 있지만 어느 것 하나 쉽지 않다. 내가 아는 한, 이 글을 쓰는 시점에서 100% 동작하는 해법은 없다.
머릿속에 성가신 생각이 하나 파고들었다. 그래, Wine에서 Windows syscall을 직접 만들면 실패한다. 그런데 Linux 규약으로 직접 syscall을 만들면 어떨까? 그건 동작해야 하는 거 아닌가? Windows 프로그램에서 Linux syscall을? 실패할 이유가 딱히 떠오르지 않는다.
정말정말 멍청한 아이디어다. 문자 그대로 아무도 필요로 하지도, 원하지도 않는다. Windows에서 실행되도록 작성된 어떤 프로그램도 원시(raw) Linux syscall을 포함하지 않으니 실용성은 0이다. 하지만 가능은 한지 꼭 보고 싶었다. 그래서 믿음직한 친구 Flat Assembler를 꺼냈다:
format PE64 NX GUI
entry start
section '.text' code readable executable
start:
; Win64 calling convention appeasement - align stack.
sub rsp,8*5
; Call the MessageBox WinAPI function to show we're really running under
; Wine.
xor r9, r9
lea r8,[msg_win]
lea rdx,[msg_win]
xor rcx,rcx
call [MessageBoxA]
; Execute a Linux syscall
mov rax, 1 ; Syscall no 1 - "write to file"
mov rdi, 1 ; File handle for stdout
mov rsi, msg_lnx ; byte buffer to write to the file
mov rdx, [msg_lnx_len] ; buffer length
syscall
; Exit cleanly.
xor rax,rax
call [ExitProcess]
; Import table stuff (not interesting, included for posterity).
section '.idata' import data readable writeable
dd 0,0,0,RVA kernel_name,RVA kernel_table
dd 0,0,0,RVA user_name,RVA user_table
dd 0,0,0,0,0
kernel_table:
ExitProcess dq RVA _ExitProcess
dq 0
user_table:
MessageBoxA dq RVA _MessageBoxA
dq 0
kernel_name db 'KERNEL32.DLL',0
user_name db 'USER32.DLL',0
_ExitProcess dw 0
db 'ExitProcess',0
_MessageBoxA dw 0
db 'MessageBoxA',0
그런데 믿기지 않겠지만? 완전히 동작했다!

재미 삼아 fork와 execve를 써서 Linux 프로그램을 실행해 보기도 했다. 새 프로그램을 실행하는 것 자체는 되는데, 원래 프로세스는 크래시 나는 것처럼 보인다:
format PE64 NX GUI
entry start
section '.data' readable writeable
msg_win db "Hello from Windows!",0
binary_path db "/usr/bin/mousepad",0
envvar0 db "DISPLAY=:1.0",0
envvar1 db "XAUTHORITY=/home/nicebyte/.Xauthority",0
envvars dq envvar0, envvar1, 0
argv dq binary_path, 0
section '.text' code readable executable
start:
; Win64 calling convention appeasement - align stack.
sub rsp,8*5
; Call the MessageBox WinAPI function
xor r9, r9
lea r8,[msg_win]
lea rdx,[msg_win]
xor rcx,rcx
call [MessageBoxA]
; Linux syscalls
mov rax, 0x39; fork
syscall
cmp rax, 0
jnz exit
mov rax, 0x3b ; execve
mov rdi, binary_path ; ELF executable file name
mov rsi, argv
mov rdx, envvars; environment vars
syscall
exit: ret
; ..import table stuff skipped...
그러니까, 그래. Wine 아래에서 실행되는 한, Windows 프로그램에서 Linux syscall을 만들 수는 있다. 완전히 쓸모없지만, 이런 프랑켄슈타인 괴물 같은 프로그램이 존재할 수 있다는 사실이 나는 웃기다.
이 글이 마음에 들었나? 더 보고 싶다면 bluesky에서 Follow 해줘!