2026-10-25
ARM 서버에서 Palworld UE4SS 실행하기 ② — x86_64 크로스 컴파일과 Docker preload 구성
ARM64 호스트에서 x86_64 UE4SS를 빌드하도록 CMake와 Rust 타깃을 맞추고, headless 빌드의 의존성과 ps_scan 링크 누락을 수정했다. Docker Compose override로 Box64 preload를 적용한 과정까지 정리한다.
게임의 아키텍처에 맞춰 빌드하기
이번 서버는 ARM64지만 Palworld 실행 파일은 x86_64다. Docker 안에서는 Box64가 게임을 실행한다. 따라서 UE4SS도 x86_64로 빌드해야 했다.
빌드 도구 자체는 ARM 서버에서 실행하고, 생성하는 기계어만 x86_64로 지정했다. 테스트한 호스트는 Ubuntu 24.04 AArch64, 크로스 GCC는 13.3.0이었다. 빌드 병렬도는 2로 제한했다.
작업 기준은 ue4ss-linux-palworld의 2397c7b 커밋이다. 아래 명령은 이 글의 수정이 적용된 작업트리를 기준으로 한다. 이번 변경을 업스트림에 병합하거나 릴리스한 것은 아니므로, 원본 저장소를 내려받는 것만으로 새 toolchain 파일과 수정 코드가 생기지는 않는다.
C/C++와 Rust의 타깃을 함께 지정하기
기본 CMake·Ninja 환경과 Rust/rustup이 준비되어 있다는 전제에서 크로스 컴파일러와 Rust 타깃을 설치한다.
sudo apt-get install -y cmake ninja-build pkg-config \
gcc-x86-64-linux-gnu g++-x86-64-linux-gnu
rustup target add x86_64-unknown-linux-gnu
이번에 추가한 cmake/toolchains/linux-x86_64-gcc.cmake의 내용은 다음과 같다.
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER x86_64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER x86_64-linux-gnu-g++)
set(CMAKE_ASM_COMPILER x86_64-linux-gnu-gcc)
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(Rust_CARGO_TARGET x86_64-unknown-linux-gnu
CACHE STRING "Rust target triple")
빌드 중 실행하는 도구는 호스트에서 찾고, 링크할 라이브러리와 헤더는 타깃 경로에서 찾도록 구분했다. 이 검색 구분은 CMake의 Linux 크로스 컴파일 문서에도 설명되어 있다. 여기의 /usr/x86_64-linux-gnu는 이번 Debian/Ubuntu 계열 툴체인의 경로다.
프로젝트에는 Rust로 만든 패턴 스캐너도 들어 있다. C++만 x86_64로 빌드하고 Rust 정적 라이브러리를 ARM64로 남겨 둘 수 없으므로 Corrosion이 사용하는 Rust_CARGO_TARGET도 명시했다.
컴파일러를 바꿀 때는 빌드 디렉터리도 새로
앞선 ARM64 빌드 디렉터리에는 네이티브 컴파일러 정보가 캐시되어 있었다. 이번 작업에서는 그 디렉터리를 보관한 뒤 같은 이름으로 새 x86_64 빌드를 구성했다. 다른 환경에서는 처음부터 별도 디렉터리를 쓰는 것도 방법이다.
cmake -B build_linux_Dev_gcc -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/linux-x86_64-gcc.cmake \
-DCMAKE_BUILD_TYPE=Game__Dev__Linux64 \
-DUE4SS_GUI_ENABLED=OFF \
-DUE4SS_INPUT_ENABLED=OFF
cmake --build build_linux_Dev_gcc --target UE4SS --parallel 2
출력 파일은 다음 위치에 생성됐다.
build_linux_Dev_gcc/Game__Dev__Linux64/lib/libUE4SS.so
file과 readelf로 확인할 수 있다.
file build_linux_Dev_gcc/Game__Dev__Linux64/lib/libUE4SS.so
readelf -h build_linux_Dev_gcc/Game__Dev__Linux64/lib/libUE4SS.so
이번에 필요한 결과는 x86-64다. ARM 서버에서 빌드했다는 이유로 AArch64가 나와야 하는 상황이 아니었다.
GUI를 끄자 드러난 공통 기능의 의존성
전용 서버에서는 GUI와 입력 기능을 끈 빌드를 사용했다. 그런데 기존 CMake는 GUI를 끄면 GUI/ 디렉터리의 소스를 통째로 제외했다. 그 안의 Dumpers.cpp에는 Lua에서도 사용하는 덤프 기능이 있었다.
수정은 GUI 디렉터리의 모든 기능을 다시 켜는 방식 대신, 필요한 소스와 화면 그리기를 구분하는 방식으로 했다.
- GUI를 제외한 소스 목록에
Dumpers.cpp를 다시 추가했다. - ImGui include와 UI 렌더링 코드는
HAS_GUI로 감쌌다. - 덤프 코드가 사용하는
is_player_controlled구현을LiveView.cpp에서Dumpers.cpp로 옮겼다. - 해당 선언도
Dumpers.hpp로 옮겼다.
이렇게 해서 GUI 없이도 Lua의 actor/mesh 덤프 기능에 필요한 코드가 링크되도록 했다. 이 기능들을 모두 실제 게임에서 호출해 검증한 것은 아니며, 이번 변경에서는 headless 빌드의 링크 의존성을 해결했다.
공유 라이브러리가 만들어져도 링크 누락은 남을 수 있었다
로더 진단 중 기존 라이브러리에 ps_scan 심볼이 없다는 오류도 발견했다. Rust 스캐너를 빌드하는 설정과, 그 결과를 UE4SS의 링크 의존성에 연결하는 설정은 따로 확인해야 했다.
deps/first/Unreal/CMakeLists.txt의 기존 링크 목록에 patternsleuth_bind를 추가했다. 아래는 기존 항목을 생략한 추가 내용이다.
target_link_libraries(Unreal PUBLIC patternsleuth_bind)
또한 Linux UE4SS 타깃에 다음 링크 옵션을 추가했다.
target_link_options(UE4SS PRIVATE "LINKER:-z,defs")
-z defs는 일반 오브젝트 파일의 해결되지 않은 심볼 참조를 공유 라이브러리 링크 단계에서도 오류로 보고하도록 한다. GNU ld 문서에서 정의한 동작이다. 모든 동적 심볼이 .so 내부에 구현되어야 한다는 뜻은 아니며, 연결한 의존 라이브러리가 제공하는 심볼은 사용할 수 있다.
수정 후에는 ps_scan 정의가 최종 산출물에 포함되었고, 엄격해진 링크 검사도 통과했다.
로드 설정은 Compose override에 두었다
호스트의 배치 경로는 다음과 같았다.
/usr/local/src/docker/palworld/palworld/libUE4SS.so
기존 bind mount 때문에 컨테이너에서는 같은 파일이 /palworld/libUE4SS.so로 보인다. x86_64 라이브러리는 ARM64 셸의 LD_PRELOAD에 넣는 대신 Box64의 preload 설정으로 전달했다. BOX64_LD_PRELOAD는 실행할 바이너리와 함께 라이브러리를 로드하도록 하는 옵션이다. Box64 공식 문서
이번에 만든 /usr/local/src/docker/palworld/docker-compose.override.yml은 다음과 같다.
services:
palworld:
environment:
BOX64_LD_PRELOAD: /palworld/libUE4SS.so:libpthread.so.0
libpthread.so.0를 함께 지정한 것은 이번에 검증한 서버 구성이다. 모든 Box64 프로그램에 반드시 필요한 보편 설정이라는 의미는 아니다.
이 디렉터리에서 docker compose up -d를 실행하면 기존 docker-compose.yml과 override가 함께 적용됐다. Compose는 기본 파일과 override를 병합하며, 같은 이름의 환경변수는 뒤에 적용한 파일의 값이 우선한다. systemd의 drop-in과 비슷한 용도로 쓸 수 있지만 병합 규칙은 Compose의 규칙을 따른다. Docker의 Compose 병합 문서
추가 진단 파일까지 -f로 지정할 때는 사용할 파일을 명시적으로 모두 나열했다. 진단용 환경변수는 영구 override와 분리해 테스트 후 제거했다.
배포는 파일 교체와 실제 실행 검증까지
라이브러리는 컨테이너를 내린 상태에서 대상 디렉터리의 임시 파일로 복사한 뒤 rename하여 교체했다. 실행 중인 프로세스가 매핑한 .so의 내용을 직접 덮어쓰는 상황을 피하기 위해서다. 배치 후에는 빌드 산출물과 배치 파일의 SHA-256이 같은지도 확인했다.
빌드 성공과 파일 배치만으로 작업이 끝나지는 않았다. 첫 x86_64 로드 뒤에는 초기화 중 segfault가 발생했고, 그 원인을 따로 추적해야 했다.
다음 편에서는 마지막으로 출력된 file_open 로그가 실제 실패 지점과 어떻게 달랐는지, 그리고 추적 로그를 어떤 경계에 넣었는지 살펴본다.
연재 글
- 1편 — ARM64 포팅 시도와 게임 아키텍처 확인
- 2편 — x86_64 크로스 컴파일과 Docker preload (현재 글)
- 3편 — file_open 이후 초기화 segfault 추적
- 4편 — Box64 콜백 슬롯 수정과 실행 검증