5.1 Lua 스크립트 실행 흐름
스크립트 언어는 소스 코드를 분석하여 바이트코드(Bytecode)로 변환한 후, 가상머신(Virtual Machine)에서 이를 실행하는 구조를 갖는다. 가상머신이 하드웨어平台的 차이를 추상화해 주므로, 동일한 코드가 다양한 운영체제와 하드웨어 환경에서 동작할 수 있다.
Lua는 레지스터 기반 가상머신을 채택한 언어다. 여기서 말하는 레지스터는 CPU의 물리적 레지스터가 아니라, 가상메모리 공간에 할당된 특정 메모리 위치를 가리키는 논리적 개념이다. 스택 기반 가상머신과 비교할 때, 레지스터 기반 방식은 사칙연산 같은 기본 연산을 한 번의 명령어로 처리할 수 있어 PUSH/POP 같은 추가 연산이 필요 없어 성능상 이점이 있다. 반면에 피연산자의 위치를 명시적으로 관리해야 하는 번거로움이 있다.
코드 실행 경로 추적
Lua의 실행 흐름을 소스 코드 수준에서 살펴보면 다음과 같은 호출 체인을 형성한다. 먼저 luaL_dofile 매크로가 파일을 로드하고 파싱을 수행한다:
// lauxlib.c:111
// 소스 파일을 로드하여 바이트코드로 변환한 후 실행
#define luaL_dofile(L, fn) \
(luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))
luaL_loadfile 함수는 내부적으로 파서를 호출하여 Proto 구조체를 생성하고, 이를 클로저(Closure)에 바인딩하여 스택에積는다:
// ldo.c:490
// 파서 함수: lexical 및 syntactic 분석을 통해 Proto 생성
static void f_parser (lua_State *L, void *ud) {
struct SParser *p = cast(struct SParser *, ud);
int c = luaZ_lookahead(p->z);
luaC_checkGC(L);
// 서명이 있으면 바이트코드 파일, 否则 Lua 소스 코드 파싱
Proto *tf = ((c == LUA_SIGNATURE[0]) ? luaU_undump : luaY_parser)
(L, p->z, &p->buff, p->name);
// 클로저 생성 및 Proto 연결
Closure *cl = luaF_newLclosure(L, tf->nups, hvalue(gt(L)));
cl->l.p = tf;
// 업밸류 초기화
for (int i = 0; i < tf->nups; i++)
cl->l.upvals[i] = luaF_newupval(L);
// 스택에 클로저 추가
setclvalue(L, L->top, cl);
incr_top(L);
}
lua_pcall은 생성된 바이트코드를 가상머신에서 실행하는 핵심 함수다:
// lapi.c:805
// 보호된 함수 호출: 바이트코드를虚拟机에서 실행
LUA_API int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc) {
struct CallS c;
c.func = L->top - (nargs + 1);
c.nresults = nresults;
int status = luaD_pcall(L, f_call, &c, savestack(L, c.func), errfunc);
adjustresults(L, nresults);
lua_unlock(L);
return status;
}
luaD_pcall은 보호된 실행 환경을 구축하고 실제 함수 호출을 수행한다:
// ldo.c:455
// 보호된 호출 프레임워크
int luaD_pcall (lua_State *L, Pfunc func, void *u, ptrdiff_t old_top, ptrdiff_t ef) {
int status = luaD_rawrunprotected(L, func, u);
// 오류 처리 로직...
}
luaD_rawrunprotected는 longjmp를 활용한 예외 처리 메커니즘을 구현한다:
// ldo.c:111
// 보호된 실행 영역: 예외 상황 처리
int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
struct lua_longjmp lj;
lj.status = 0;
lj.previous = L->errorJmp;
L->errorJmp = &lj;
LUAI_TRY(L, &lj,
(*f)(L, ud); // 여기서 f_call 호출
);
L->errorJmp = lj.previous;
return lj.status;
}
f_call은 실제 함수 호출을 위한 환경을 구성한다:
// lapi.c:798
// 함수 호출 핸들러
static void f_call (lua_State *L, void *ud) {
struct CallS *c = cast(struct CallS *, ud);
luaD_call(L, c->func, c->nresults);
}
luaD_call은 Lua 함수를 실행하기 전에 사전准备工作을 수행하고, Lua 함수인 경우 luaV_execute를 호출한다:
// ldo.c:369
// Lua 함수 호출 실행
void luaD_call (lua_State *L, StkId func, int nResults) {
if (luaD_precall(L, func, nResults) == PCRLUA)
luaV_execute(L, 1); //虚拟机 실행 시작
L->nCcalls--;
luaC_checkGC(L);
}
luaD_precall은 함수 호출에 필요한 CallInfo를 설정하고 실행 환경을 준비한다:
// ldo.c:264
// 함수 호출 전 환경 구성
int luaD_precall (lua_State *L, StkId func, int nresults) {
LClosure *cl = &clvalue(func)->l;
L->ci->savedpc = L->savedpc;
if (!cl->isC) { // Lua 함수인 경우
StkId base;
Proto *p = cl->p;
CallInfo *ci = inc_ci(L);
ci->func = func;
L->base = ci->base = base;
ci->top = L->base + p->maxstacksize;
lua_assert(ci->top <= L->stack_last);
L->savedpc = p->code;
ci->tailcalls = 0;
ci->nresults = nresults;
// 미전달 인자는 nil로 초기화
for (StkId st = L->top; st < ci->top; st++)
setnilvalue(st);
L->top = ci->top;
return PCRLUA;
}
}
luaV_execute는 Lua 가상머신의 핵심 실행 루프다:
// lvm.c:373
//虚拟机 실행 루프
void luaV_execute (lua_State *L, int nexeccalls) {
LClosure *cl;
StkId base;
TValue *k;
const Instruction *pc;
reentry:
lua_assert(isLua(L->ci));
pc = L->savedpc;
cl = &clvalue(L->ci->func)->l;
base = L->base;
k = cl->p->k;
// 메인 인터프리터 루프
for (;;) {
const Instruction i = *pc++;
StkId ra;
// 후크 인터페이스 처리
if ((L->hookmask & (LUA_MASKLINE | LUA_MASKCOUNT)) &&
(--L->hookcount == 0 || L->hookmask & LUA_MASKLINE)) {
traceexec(L, pc);
if (L->status == LUA_YIELD) {
L->savedpc = pc - 1;
return;
}
base = L->base;
}
ra = RA(i);
lua_assert(base == L->base && L->base == L->ci->base);
lua_assert(base <= L->top && L->top <= L->stack + L->stacksize);
lua_assert(L->top == L->ci->top || luaG_checkopenop(i));
// 명령어 디스패치
switch (GET_OPCODE(i)) {
case OP_MOVE: {
setobjs2s(L, ra, RB(i));
continue;
}
// 기타 명령어 처리...
}
}
}
함수 실행이 완료된 후 luaD_poscall이 이전 호출 환경으로 복원한다:
// ldo.c:342
// 함수 반환 후 환경 복원
int luaD_poscall (lua_State *L, StkId firstResult) {
if (L->hookmask & LUA_MASKRET)
firstResult = callrethooks(L, firstResult);
CallInfo *ci = L->ci--;
StkId res = ci->func;
int wanted = ci->nresults;
L->base = (ci - 1)->base;
L->savedpc = (ci - 1)->savedpc;
// 결과 이동
for (int i = wanted; i != 0 && firstResult < L->top; i--)
setobjs2s(L, res++, firstResult++);
while (i-- > 0)
setnilvalue(res++);
L->top = res;
return (wanted - LUA_MULTRET);
}
5.2 호출 스택과 데이터 구조
각 Lua 가상머신 인스턴스는 lua_State 구조체로 표현되며, TValue 배열로 구현된 스택을 사용하여 함수 호출과 데이터 저장을 관리한다.
lua_State 내부에는 고정 크기의 CallInfo 배열(base_ci[])이 존재하고, 현재 실행 중인 함수에 대한 정보는 ci 포인터를 통해 접근한다. luaD_precall 함수 호출 전 반드시 다음 작업들이 수행된다:
- 현재 실행 중인 명령어 위치(savedpc)를 현재 CallInfo에 저장 - 함수 종료 후 실행 흐름 복원을 위함
- 호출할 함수의 base와 top 값을 파라미터 개수를 기준으로 계산
- base_ci 배열에서 새로운 CallInfo를 할당받아 함수 호출 준비
lua_State의 top과 base 포인터는 항상 현재 실행 중인 함수의 스택 영역을 가리킨다.
5.3 파싱 단계의 데이터 구조
FuncState 구조체는 파싱 과정에서 함수 관련 정보를 임시로 저장한다. 이 구조체는 prev 포인터를 통해 체인 형태로 연결되어, 중첩된 함수 파싱 정보를 관리한다.
FuncState와 같은 임시 구조체들은 최종적으로 Proto 구조체에 명령어들을 생성하기 위한 중간 역할을 수행한다.
Proto 구조체의 주요 구성요소:
- 상수 배열: 함수에서 사용되는 상수 값 저장
- OpCode 배열: 파싱 후 생성된 바이트코드 명령어 저장
- 지역 변수 배열: 함수 내 지역 변수 정보 저장
- 업밸류 배열: 클로저에서 참조되는 외부 변수 정보 저장
FuncState와 Proto의 관계를 보여주는 예시:
-- 함수 정의 예시
local function a() -- fsa (FuncState a)
local function b() -- fsb (FuncState b)
end
end
5.4 명령어 형식 정의
lopcodes.h 파일에 명령어 형식과 필드 접근 매크로가 정의되어 있다:
/*
** 명령어 인자의 크기와 위치 정의
*/
#define SIZE_C 9
#define SIZE_B 9
#define SIZE_Bx (SIZE_C + SIZE_B)
#define SIZE_A 8
#define SIZE_OP 6
#define POS_OP 0
#define POS_A (POS_OP + SIZE_OP)
#define POS_C (POS_A + SIZE_A)
#define POS_B (POS_C + SIZE_C)
#define POS_Bx POS_C
// Opcode 추출 및 설정
#define GET_OPCODE(i) (cast(OpCode, ((i)>>POS_OP) & MASK1(SIZE_OP,0)))
#define SET_OPCODE(i,o) ((i) = (((i)&MASK0(SIZE_OP,POS_OP)) | \
((cast(Instruction, o)<>POS_A) & MASK1(SIZE_A,0)))
#define SETARG_A(i,u) ((i) = (((i)&MASK0(SIZE_A,POS_A)) | \
((cast(Instruction, u)<>POS_B) & MASK1(SIZE_B,0)))
#define SETARG_B(i,b) ((i) = (((i)&MASK0(SIZE_B,POS_B)) | \
((cast(Instruction, b)<>POS_C) & MASK1(SIZE_C,0)))
#define SETARG_C(i,b) ((i) = (((i)&MASK0(SIZE_C,POS_C)) | \
((cast(Instruction, b)<>POS_Bx) & MASK1(SIZE_Bx,0)))
#define SETARG_Bx(i,b) ((i) = (((i)&MASK0(SIZE_Bx,POS_Bx)) | \
((cast(Instruction, b)<
lopcodes.c에는 각 명령어의 모드 정보가 정의되어 있다:
// 명령어 모드 정의 배열
const lu_byte luaP_opmodes[NUM_OPCODES] = {
/* T A Barg Carg mode opcode */
opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_MOVE */
,opmode(0, 1, OpArgK, OpArgN, iABx) /* OP_LOADK */
,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_LOADBOOL*/
,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_LOADNIL */
// 추가 명령어...
};
각 필드의 의미:
- T: 논리 테스트 관련 명령어 여부 - 명령어 실행 후 PC 증가 여부 결정
- A: 결과를 R(A)에 저장할지 여부
- B/C: 인자 형식 지정
- OpArgN: 인자 미사용
- OpArgU: 인자 사용됨
- OpArgR: 레지스터 또는 점프 오프셋을 나타내는 인자
- OpArgK: 상수를 참조하는 인자
- mode: 명령어 형식 (iABC, iABx 등)
가상머신 실행 중 데이터 접근을 위한 매크로:
// lvm.c의 일반적인 작업 매크로
#define runtime_check(L, c) { if (!(c)) break; }
#define RA(i) (base+GETARG_A(i))
// 스택 재할당 후 사용 가능한 레지스터 접근 매크로
#define RB(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgR, base+GETARG_B(i))
#define RC(i) check_exp(getCMode(GET_OPCODE(i)) == OpArgR, base+GETARG_C(i))
#define RKB(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgK, \
ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i))
#define RKC(i) check_exp(getCMode(GET_OPCODE(i)) == OpArgK, \
ISK(GETARG_C(i)) ? k+INDEXK(GETARG_C(i)) : base+GETARG_C(i))
#define KBx(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgK, k+GETARG_Bx(i))
5.5 명령어 실행 루프
가상머신의 핵심인 명령어 실행 루프의 구조:
void luaV_execute (lua_State *L, int nexeccalls) {
LClosure *cl;
StkId base;
TValue *k;
const Instruction *pc;
reentry: // 재진입 지점
lua_assert(isLua(L->ci));
pc = L->savedpc; // 현재 실행할 명령어 위치
cl = &clvalue(L->ci->func)->l; // 현재 함수 클로저
base = L->base; // 현재 스택 베이스 주소
k = cl->p->k; // 상수 테이블
// 인터프리터 메인 루프
for (;;) {
// 명령어 페치 및 디스패치...
}
}
5.6 디버깅 도구 활용
Lua 가상머신 디버깅 시 핵심 함수에 브레이크포인트를 설정한다:
- 명령어 생성: luaK_code 함수
- 명령어 실행: luaV_execute 함수
바이트코드를 분석하기 위해 ChunkSpy 도구를 사용할 수 있다:
ChunkSpy 설치 시 아키텍처 설정 문제 발생할 수 있다. 32비트에서 64비트 환경으로 변경 시 다음 수정이 필요한다:
// size_t 크기 변경: 4에서 8로
size_size_t = 8
ChunkSpy 실행 예시:
$ lua -v
Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio
$ lua ChunkSpy.lua --source test.lua --brief
; source chunk: test.lua
; x86 standard (32-bit, little endian, doubles)
; function [0] definition (level 1)
; 0 upvalues, 0 params, 2 stacks
.function 0 0 2 2
.local "a" ; 0
.const 1 ; 0
[1] loadk 0 0 ; 1
[2] return 0 1
; end of function
테스트에 사용된 Lua 소스:
local a = 1
출력 형식 설명:
- ;: 주석
- .:数据类型 및 구조체 정의
- [숫자] Opcode: 명령어 번호와 연산 코드