ZERO development cost
추가 개발 없이 원하는 정보를 바로 수집하도록 설정
ZERO integration cost
개발팀이 연동을 위해 한 일: ZERO 연동 라이브러리 추가 및 초기화 함수 호출
ZERO additional patch
시연으로 확인 하였듯이, 새로운 내용을 수집하려 할 때 패치가 필요 없음
ZERO delay
실시간으로 새로운 정보를 수집하고, 실시간으로 확인 가능
ZERO risk
실수를 하더라도 게임 서비스에는 장애가 발생하지 않게 여러 겹의 보호 장치
ZERO 서버
게임
클라이언트
PDB
ZERO Web
C++ EE 변환 C++ EE 실행
Function
Hooking
PDB Inspect명령 지시 및 확인
* 게임 서버를 거치지 않고 게임 클라이언트가 직접 연결하여
관련 정보를 수집하게 구현되었습니다. 게임 서버에 연동하여
서버의 내용을 얻어오는 방식도 차후 구현될 예정입니다.
PDB
Program Database file
컴파일러가 어떻게 실행 파일을 생성했는지 저장
Visual Studio를 통해 디버깅하기 위해 필요
각 심볼의 이름, 타입, 주소 값 등을 저장함
dbghelp 또는 DIA SDK 를 통해 값을 읽을 수 있다.
DIA SDK
PDB 파일을 읽기 위한 API
COM 기반
트리 형태로 심볼 정보를 접근 + 심볼 ID로 검색
source = CoCreateInstance(CLSID_DiaSource)
session = source->LoadDataForExe(“Game.exe”, …)
session->get_globalScope(&global)
http://github.com/ipkn/wdd Symbols.cpp
IDiaSymbol 속성
symTag 어떤 종류의 심볼인지
symIndexId session->symbolById로 검색가능한 ID
name 심볼 이름 (optional)
typeId 이 심볼의 타입을 나타내는 심볼의 ID
virtualAddress 심볼의 주소
session->put_loadAddress 로 기준 주소를 설정가능
dataKind
locationType
registerId
offset
트리 구조
get_globalScope()로 얻은 심볼로부터
함수 .symTag = SymTagFunction
전역 변수 .symTag = SymTagData, dataKind = DataIsStaticMember
클래스 .symTag = SymTagUDT, udtKind
트리 구조 - 함수
.symTag == SymTagFunction
.type 함수 타입에 대한 심볼
.type.type 함수 리턴 타입
.virtualAddress 함수 시작 주소
인자
SymTagData, dataKind = DataIsParam
.type 인자의 타입
로컬 변수
SymTagData, dataKind = DataIsLocal
트리 구조 - 클래스
부모 클래스, 멤버 변수, 멤버 함수, …
class C : public A, public B { … } c;
(A*)&c
(B*)&c 서로 다른 주소값
UDT(C)
BaseClass(A), .offset = (A*)로 변환할때 this에 더해줘야 하는 값
BaseClass(B), .offset
std::string (SymTagUDT)
std::_String_alloc<…> (SymTagBaseClass)
…
size_type (SymTagTypeDef)
value_type (SymTagTypeDef)
…
size (SymTagFunction) 인자가 없는 멤버함수: this만 존재
this (SymTagData)
(SymTagFuncDebugStart, SymTagFuncDebugEnd, SymTagInlineSite, SymTagCallee)
resize (SymTagFunction) 인자가 2개
this (SymTagData)
_Newsize (SymTagData)
_Ch (SymTagData)
C++용 인터페이스 작성
각 속성을 멤버변수로, (optional 타입)
findChildren을 iterator로 래핑
for(auto base : s.AllBaseClass())
{
if (c.name)
*c.name;
}
DIA SDK 주의점
msdia140.dll 등록 필요 (버전에 따라 120, 100, …)
symsrv.dll
심볼 서버 사용시 필요
없으면 심볼을 찾지 못한다는 에러가 리턴
symIndexId는 세션 별로 부여됨
함수 호출 순서에 따라 다른 값이 주어짐
다른 언어/환경으로의 확장
모바일 기기 지원 고려 중
DWARF 포맷 – gcc, clang
clang은 PDB도 지원 (mostly)
C# (Unity) – 리플렉션 활용
변환: C++ 표현식 파서
C++의 매우 작은 일부분만 지원
간단한 Recursive Descent Parser (링크)
필요한 기능 만큼만 직접 구현
* 파서를 만드는 방법에 대한 내용은 컴파일러 책 등을 참고 바랍니다.
C++ 식 → Lua 트랜스파일러
변수 참조, 캐스팅, 함수 호출
실행: 변환된 코드를 전송 후 실행
바로 계산이 이루어지는 게 아니라
접속된 클라이언트 내에서 실행 되어야함
실행할 내용을 동적 언어 코드(Lua)로 변환 후 전송
값의 표현
at(주소) 또는 Lua 값
take_addr(at(100)) === 100
at(100):member(4) === at(104)
read(100, ‘i’) === 100
read(at(100), ‘i’) === *(int*)100
API 확장
va_to_a Symbol 주소를 실제 메모리 주소로 변환
take_addr 주소값 얻기. & 연산자 구현에 필요
at.member 멤버에 해당하는 객체 리턴
read 메모리 읽기
reg 레지스터 값 읽기
get/set 계산에 필요한 값 불러오기/저장하기
read_mbcs, read_ucs2, ga_...,
va_to_a
심볼에 저장된 주소
= 해당 모듈이 메모리 주소 0에 로딩 되었을 때 기준
ASLR
Address space layout randomization
해킹 방지 기법 중 하나
각 클라이언트는 다른 메모리 주소에 로딩될 수 있다
GetModuleHandle(nullptr) 실행중인 exe가 로딩된 주소
변환 결과물: LuaValue
`C++ expr` → Lua code + type
`1` → 1, IntType
A a
`a` → at(addr of a), A
a는 전역 변수, 로컬 변수, 함수(함수처리는 이후에!)
재귀적 정의
X → code(X), type(X) B X::b
`&X` → take_addr(code(X)), type(X)*
`*X` → at(code(X)), type(X).pointing_type()
`X->b` → at(read(code(X),’p’)):member(
offset of b from type(X)), B
at(at(…)) 꼴을 없애려고 read 사용
`X.b` → ?
C가 유저 타입이면
`(C)X` → code(X), C
이런 식으로 double을 int로 캐스팅 한다면 버그가
발생하지만, 작업량 대비 효과가 적어 차후 구현.
타입 변환
함수 호출 필요성
단순히 변수 읽는 것 만으로는 부족
프로그래머가 익숙한 방식으로 사용할 수 있게됨
예) 싱글턴
Singleton<A>::m_spInstance
Singleton<A>::GetInstance()
* 함수 스태틱으로 구현한 경우 함수 호출 없인 아예 접근 불가
실행단에선 호출할 함수의 프로토타입을 모름
- 어떻게 인자를 넘기고
- 어떻게 결과값을 받고
- Calling convention은 뭔지
모두 알려줘야 한다.
함수 호출 (1)
Out parameter (구현 중)
bool GetPosition(vector3& pos)
Lua 단에서 따로 메모리를 할당한 후 해당 포인터를 전달
계산 종료 후 관련 메모리 해제
local buf = buffer(12)
call(`code for GetPosition`, buf)
buf:member_at(4)
User defined type (미해결)
포인터, 레퍼런스로는 문제없이 활용
value type으로 사용시 어려움 발생
// COW 최적화 적용된 전용 스트링 클래스 String
String GetName()
소멸자를 불러주지 않으면 좀비 스트링이 생기게 된다.
인자로 넣는 경우라면? (더욱 복잡)
한계
Inline 되어 사라진 함수
최적화를 통해 생각보다 많은 함수가 inline됨
버전, 컴파일러 옵션에 따라 비일관적인 동작
LTCG와 WPO 켜면
VC버전에 따라
PDB와 실제 출력된 코드가 다른 경우가 있었음 (매우 가끔)
손상된 PDB 파일이 나온 경우도 있었음 (1번)
아마도 컴파일러 버그? ㅠㅠ
활용 - 심볼 탐색 및 식 계산
PDB와 C++ EE를 쉽게 활용하기 위한 툴 작성 및 적용
ZEROConsole x.exe x.pdb
실행중인 프로세스에 연결
ZEROConsole x.dmp x.pdb
풀덤프에 연결
ZEROConsole - x.pdb
PDB 정보만 확인
활용: 풀덤프 탐색 (1)
라이브 게임 서버 덤프
던전 container에서 특정 던전을 찾아 처리 중 크래쉬
원소가 남아있다면 → 메모리 덮어쓰기 버그
원소가 없다면 → 멀티 쓰레드 버그
활용: 풀덤프 탐색 (2)
전역 해시테이블에 특정 원소가 있는지 확인해야 하는 상황
원소 80만개 – 손으로 확인 불가
소스 분석
STLPort 기반 unordered_map
전체 데이터는 Singly Linked List로 표현됨
활용: 풀덤프 탐색 (3)
Full Dump는 모든 메모리 정보를 가짐
C++ EE를 구현함에 따라
Lua 코드로 덤프를 자유자재로 탐색할 수 있게 됨
루프 돌면서 해시테이블을 확인
디버깅 시나리오 확인에 도움
유저 접속 시 랜덤 키 할당, 해당 키로 분배
수집 내용별 비율이 따로 존재
특정 유저에게만 수집 내용이 몰리지 않을까 하는 우려
합쳐진 키를 사용하여 분배하게 작성
(유저 키 + 수집 내용 키)
유저 샘플링
MySQL JSON column
On-demand Data Extractor를 위한
비정형 데이터 저장에 사용
MySQL 5.7.8 이상
VIEW를 만들면 Readonly SQL 테이블 처럼 활용 가능
• * 다른 RDBMS들도 JSON 타입을 지원하나
• 사용성이 MySQL이 좋았음
장애 대비 (1)
장애가 발생해선 안됨
개발팀 외부에서 만들어진 라이브러리
아무리 좋은 물건이더라도
크래쉬를 유발하거나 성능 문제를 일으키면
적용을 유보하거나 롤백할 수 밖에 없음
장애 대비 (2)
수집 쓰로틀링
지나치게 많이 수집되면 자동으로 수집양을 줄인다
의도치 않은 경우를 대비
크래쉬 상황 대비
SEH로 감싸기
기능별로 lua_State를 나눠서 사용
장애 대비 (3)
여전히 실수하면 문제가 발생할 여지는 있음
적용 대상을 적은 비율부터 점차적 확대 가능하게
개발자가 배포된 클라이언트로
ZERO 기능을 테스트 해 볼 수 있는 환경 제공
Future Works (1)
Hot Patch (실행 중 코드 수정)
실시간 버그 수정
예: 비정상적인 인자가 들어올 때 return 해버리게
JYP와 연동되어
컨텐츠별 성능 정보 추적 및 활용