OCaml Internals
Preface
복귀가 늦었습니다. 짧은 시간 내에 타임어택 형태로 진행되는 문제 분석은 LLM이 우위를 점한지 오래입니다. 따라서 장기간 진득하게 하나의 시스템을 잡고 분석하는 형태로 글을 작성해보고자 합니다. CTF의 “감”을 계속 유지하면서도, LLM의 Dominance로 인한 CTF의 번아웃을 방지하기 위한 저 나름대로의 재활 치료입니다.
Introduction
C언어와 같은 절차적 프로그래밍 언어는 현대의 컴퓨터 아키텍처와 밀접한 관련을 가지고 있습니다. 프로그램 카운터가 명령어를 따라 움직이고, 메모리 주소에 직접 접근이 가능합니다. 이에 반해, OCaml과 같은 함수형 프로그래밍 언어는 고수준의 추상화를 제공합니다. Lambda Calculus에 의미론적 기반을 두고 있기에 프로그램의 수행은 함수의 연속된 평가로 이루어집니다. 이러한 Lambda Calculus 덕분에, 함수를 객체로 취급할 수 있고, 고차 함수를 지원하며, Currying, Lazy Evaluation과 같은 기능을 가능하게 합니다.
하지만 결국 OCaml로 작성된 코드를 실행시키기 위해서는 저수준으로의 이동이 필요합니다. OCaml의 추상적 표현을 컴퓨터는 어떻게 이해할까요? 완전히 달라보이는 두 패러다임이 어떻게 연결되는지 살펴보겠습니다.
Note
OCaml으로 작성된 코드를 실행하기 위해, OCaml 바이트코드로 컴파일 (ocamlc)하는 방법과 네이티브 코드로 (ocamlopt) 컴파일하는 방법이 있습니다.
전자의 경우 ocamlrun 인터프리터가 필요하지만, 후자의 경우 OCaml 런타임을 동봉한 기계어 실행 파일이 생성됩니다. 본 글에서는 OCaml 런타임과 관련된 내용을 다루므로, Stand-alone으로 실행 가능한 ocamlopt의 출력을 분석 대상으로 삼겠습니다.
주로 OCaml/runtime 디렉토리의 소스코드를 참조하여 분석했습니다.
Pre-requisites
mlvalues.h 파일은 OCaml에서 객체를 관리하기 위한 핵심적인 구조체 및 매크로를 정의하고 있습니다.
OCaml 컴파일러로 타입 체킹이 완료된 이후, OCaml 런타임에서 사용되는 모든 값들은 value 타입으로 표현됩니다.
Core Definitions
마찬가지로 mlvalues.h의 상단에서 정의되어 있습니다
word: 16/32bit 시스템에서 4바이트, 64bit 시스템에서 8바이트로 정의되는 기본 단위입니다.long:word와 동일한 크기를 가지는 정수형입니다.val: OCaml에서 “무언가” 를 나타내는 타입으로,long혹은block에 대한 포인터입니다.block: 값이 할당되어있는 메모리 블럭을 나타냅니다.- 항상 헤더가 존재하며, 여러 개의
field(word-size의 메모리 공간으로val이 저장됨)를 가질 수 있습니다.
- 항상 헤더가 존재하며, 여러 개의
Header
블럭의 헤더는 다음과 같이 정의되어 있으며, 64비트 시스템을 기준으로 설명하겠습니다.
For 64-bit architectures:
+----------+--------+-------+-----+
| reserved | wosize | color | tag |
+----------+--------+-------+-----+
bits 63 64-R 63-R 10 9 8 7 0
tag: 8bit, 블록의 유형을 나타냅니다. 예를 들어, 튜플, 레코드, 배열 등 다양한 데이터 구조를 구분하는 데 사용됩니다.color: 2bit, OCaml GC(Garbage Collection)에서 객체의 상태를 나타내는 데 사용합니다. 본 문서에서는 자세히 다루지 않겠습니다.wosize: 상위 비트, 블록이 가지고 있는 필드의 개수를 나타냅니다. “
Is it a block?
Ocaml에서 value는 31비트의 정수 또는 포인터로 표현됩니다. 익히 알고 있는 int는 32비트이지만, OCaml에서는 상위 31바이트를 사용해 정수를 표현하고, 최하위 1바이트는 태그로 사용됩니다.
태그는 해당 value가 정수인지, 블록에 대한 포인터인지 나타냅니다.
/* Longs vs blocks. */
#define Is_long(x) (((x) & 1) != 0)
#define Is_block(x) (((x) & 1) == 0)
하위 1비트를 검사하여 value가 정수인지 블록인지 구분합니다. Is_block을 만족한다면, 해당 value는 블록에 대한 포인터로 해석됩니다. Word-size Align이 보장되므로, 최하위 비트에 대한 정보가 필요하지 않습니다.
How ‘Object’ are represented in OCaml
간단한 내용들이기에 짧게 넘어가겠습니다. OCaml에서 객체는 value 타입으로 표현됩니다. value는 31비트의 정수 또는 블록에 대한 포인터로 표현됩니다. OCaml 런타임은 이러한 value들을 관리하기 위해 다양한 데이터 구조와 메모리 레이아웃을 사용합니다.
Integers and Characters
절차지향적 언어에서 확인할 수 있는 간단한 정수형, 실수형, 문자열부터 어떤 식으로 표현되는지 살펴보겠습니다. Block으로 Allocate되지 않는, 가장 간단한 Tagged Value로 표현됩니다.
let my_integer = 42
let my_char = 'c'
IDA에서 확인했을 때, 다음과 같이 초기화됩니다.
caml_initialize(camlTest, 85LL); // 85LL == 42 << 1 | 1
caml_initialize(&camlTest[1], 199LL); // 199LL == 99 << 1 | 1 == 'c' << 1 | 1
Strings
let my_string = "Hello"
IDA에서 확인했을 때, 다음과 같이 초기화됩니다.
Warning
데이터는 caml{binary_name}.{index} 형태(ex: camlTest.14)로,
함수는 caml{binary_name}.{func_name}_{index} 형태(ex: camlTest.fib_281)로 초기화됩니다.
nm 을 찍어서 확인해 봅시다.
caml_initialize(&camlTest[2], &camlTest_2); // 0xBC288
참조되는 포인터 위치를 확인해 봅시다.
| Address | Bytes | Info |
|---|---|---|
0xBC280 | FC07 00 00 00 00 00 00 | Header |
0xBC288 | 48 65 6C 6C 6F00 00 02 | String |
Word-size align을 맞추기 위해 가장 마지막 바이트에 NULL Count를 넣어 문자열의 크기를 나타내고 있습니다. String Tag는 Header에서 0xFC로 표현됩니다.
Floating-point
let my_float = 3.1415
IDA에서 확인했을 때, 다음과 같이 초기화됩니다.
caml_initialize(&camlTest[3], &camlTest_3); // 0xBC180
참조되는 포인터 위치를 확인해 봅시다.
| Address | Bytes | Info |
|---|---|---|
0xBC178 | FD78 00 00 00 00 00 00 | Header |
0xBC180 | 6F 12 83 C0 CA 21 09 40 | (double) 3.1415 |
Float Tag는 Header에서 0xFD로 표현됩니다. Float Array의 경우, Header에서 0xFE로 표현됩니다.
”Flat” objects
중첩되지 않은 데이터 구조입니다. 대표적으로 튜플이 있습니다.
let my_tuple = (10, 20) (* int * int *)
Nested objects
중첩된 데이터 구조 (재귀적인 구조) 입니다. 블럭의 Field가 또 다른 블럭을 가리키는 형태로 구현되어 있습니다.
Nested Tuples
let another_tuple = (10, (20, 30), 40) (* int * (int * int) * int *)
메모리에 다음과 같이 나타납니다. 블럭 헤더의 wosize 필드와, Tagged Integer에 주의하며 살펴봅시다.

Lists
OCaml에서 list의 패턴 매칭을 시도할 때 hd::tl 형태로 표현됩니다. 마찬가지로 list가 재귀적으로 정의된 데이터 구조이기 때문입니다.
let my_list = [10; 20; 30; 40]
메모리에 다음과 같이 나타납니다. 각각의 블럭은 길이 2 (hd와 tl 포인터)로 구성되어 있습니다.
- 본 예시에서는
int만을 사용하므로,hd부분이 Tagged Integer로 표현됩니다. (int가 아닌 타입을 사용한다면, 블럭에 대한 포인터로 표현될 것입니다.) tl부분은 다음 블럭에 대한 포인터로 표현됩니다. 리스트의 끝은1 == (0 << 1 | 1)으로 표현됩니다.NULLpointer가 아닌, Tagged Integer로 표현된다는 점에 주의합시다.

How ‘Functions’ are represented in OCaml
본 문서의 핵심
일반적으로 절차지향적 프로그래밍 언어에서 사용하는 함수는 함수를 반환하지 않습니다.
예를 들어, int add(int a, int b, int c)의 함수는 반드시 호출될 때 3개의 int 형 인자를 제공해야만 합니다. OCaml의 Notation을 빌리자면, (int * int * int) -> int로 나타낼 수 있겠습니다.
OCaml에서 같은 함수를, Curry하여 int -> int -> int -> int로 표현할 수 있습니다. 즉, add 함수는 int를 받아서 int -> int -> int 형태의 함수를 반환하고,
다시 int를 받아서 int -> int 형태의 함수를 반환하고, 마지막으로 int를 받아서 int를 반환하는 형태로 표현할 수 있습니다. 함수에 인자를 부분적으로 적용하여 새로운 함수를 생성하는 Currying의 개념이 OCaml에서 함수가 어떻게 표현되는지 이해하는 데 핵심적인 역할을 합니다.
OCaml에서 함수는 일급 객체로 취급됩니다. 함수를 호출할 때, 함수가 호출된 문맥을 관리하기 위한 Closure를 함께 생성합니다. Closure과 관련된 OCaml Internal 자료구조는, 헤더의 첫 바이트가 0xF7으로 시작합니다.
Calling Conventions
Note
System V AMD64 ABI 기준으로 설명합니다.
Using Registers and Tail-Call Optimization
C ABI에서는 rdi, rsi, rdx, rcx, r8, r9 레지스터, (그리고 필요하다면 스택)을 사용해서 함수 인자를 전달합니다.
OCaml은 이와 다른 컨벤션을 사용합니다.
OCaml 컴파일러는 순서대로 rax, rbx, rsi, rdi, rdx, rcx, r8, r9, r12, r13, r10, r11, rbp 를 사용하여 전달합니다. Floating-point의 경우 xmm0 부터 xmm15 까지의 레지스터를 모두 사용합니다. (초과되는 인자의 경우, 스택에 저장합니다.)
최대한 모든 인자를 레지스터를 통해 전달하는 것은 OCaml과 같은 함수형 프로그래밍 언어 패러다임에 있어서 중요합니다. 함수가 함수를 호출하는 과정에서 Call Stack이 필연적으로 깊어질 수 밖에 없습니다. Call Stack이 깊어지는 것을 방지하기 위해 Tail-Call Optimization을 적용하게 되는데, 지역 변수를 스택에 저장한다면 Tail-Call Optimization이 어려울 것입니다. 따라서 OCaml은 최대한 많은 인자를 레지스터로 전달하여 Tail-Call Optimization이 가능하도록 설계되어 있습니다.
물론, OCaml 또한 SystemV AMD64 ABI 환경에서 정의된 외부 인터페이스와의 호환성 (libc의 사용 등)을 보장해야 합니다.
caml_c_call, caml_c_call_stack_args, caml_c_call_copy_stack_args 의 함수가 호환성 레이어를 담당합니다. 인자 순서 재정렬, ABI 간 Caller/Callee-saved Register가 달라서 발생하는 Register Contamination 처리를 수행합니다.
Register for Memory Management
r14, r15 레지스터는 OCaml Runtime에서 특별한 용도로 사용됩니다. 각각 Domain State Pointer 와 Allocation Pointer이라는 이름이 붙어 있고, OCaml 런타임이 메모리를 관리하는 데 사용됩니다. OCaml 런타임은 메모리 할당과 가비지 컬렉션을 관리하기 위해 이러한 레지스터를 활용합니다.
Calling Functions
let rec gcd a b = if b = 0 then a else gcd b (a mod b) [@@inline never]
let gcd4 x y z w = gcd x (gcd y (gcd z w)) [@@inline never] (* i -> i -> i -> i => i *)
let () =
let tmp1 = gcd4 (int_of_string Sys.argv.(1)) in
Printf.printf "1st arg: %d\n" (int_of_string Sys.argv.(1));
let tmp2 = tmp1 (int_of_string Sys.argv.(2)) in
Printf.printf "2nd arg: %d\n" (int_of_string Sys.argv.(2));
let tmp3 = tmp2 (int_of_string Sys.argv.(3)) in
Printf.printf "3rd arg: %d\n" (int_of_string Sys.argv.(3));
let final = tmp3 (int_of_string Sys.argv.(4)) in
Printf.printf "4th arg: %d\n" (int_of_string Sys.argv.(4));
Printf.printf "Result: %d\n" (final)
디스어셈블리의 결과를 보면, 다음과 같은 함수 호출을 확인할 수 있습니다.
첫 번째 호출 (let tmp1 = gcd4 (int_of_string Sys.argv.(1))) 은 메모리를 다음과 같이 초기화합니다.
0x4966f: lea rdi,[r15+0x8]
0x49673: mov QWORD PTR [rsp+0x8],rdi
0x49678: mov QWORD PTR [rdi-0x8],0x14f7
0x49680: lea rsi,[rip+0xfffffffffffff889] # 48f10 <caml_curry3>
0x49687: mov QWORD PTR [rdi],rsi
0x4968a: movabs rsi,0x300000000000007
0x49694: mov QWORD PTR [rdi+0x8],rsi
0x49698: lea rsi,[rip+0xfffffffffffffcd1] # 49370 <camlTest.fun_573>
0x4969f: mov QWORD PTR [rdi+0x10],rsi
0x496a3: mov QWORD PTR [rdi+0x18],rax
0x496a7: mov QWORD PTR [rdi+0x20],rbx
0x496ab: mov edi,0x1
camlTest_fun_573 은 다음과 같이 디스어셈블됩니다
0x49370: mov rdx,rax
0x49373: mov rcx,rbx
0x49376: mov r8,rdi
0x49379: mov rax,QWORD PTR [rsi+0x18]
0x4937d: mov rbx,rdx
0x49380: mov rdi,rcx
0x49383: mov rsi,r8
0x49386: jmp 49320 <camlTest.gcd4_286>
두 번째 호출 (let tmp2 = tmp1 (int_of_string Sys.argv.(2))) 은 다음과 같이 메모리를 초기화합니다.
0x49747: lea rdi,[r15+0x8]
0x4974b: mov QWORD PTR [rsp+0x8],rdi
0x49750: mov QWORD PTR [rdi-0x8],0x14f7
0x49758: lea rsi,[rip+0xfffffffffffff8a1] # 49000 <caml_curry2>
0x4975f: mov QWORD PTR [rdi],rsi
0x49762: movabs rsi,0x200000000000007
0x4976c: mov QWORD PTR [rdi+0x8],rsi
0x49770: lea rsi,[rip+0xfffffffffffffc19] # 49390 <camlTest.fun_582>
0x49777: mov QWORD PTR [rdi+0x10],rsi
0x4977b: mov QWORD PTR [rdi+0x18],rax
0x4977f: mov QWORD PTR [rdi+0x20],rbx
0x49783: mov edi,0x1
camlTest_fun_582는 다음과 같이 디스어셈블됩니다.
0x49390: mov rdx,rax
0x49393: mov rsi,rbx
0x49396: mov rbx,QWORD PTR [rdi+0x18]
0x4939a: mov rax,QWORD PTR [rdi+0x20]
0x4939e: mov rax,QWORD PTR [rax+0x18]
0x493a2: mov rdi,rdx
0x493a5: jmp 49320 <camlTest.gcd4_286>
세 번째 호출 (let tmp3 = tmp2 (int_of_string Sys.argv.(3))) 은 다음과 같이 메모리를 초기화합니다.
0x4981f: lea rdi,[r15+0x8]
0x49823: mov QWORD PTR [rsp+0x8],rdi
0x49828: mov QWORD PTR [rdi-0x8],0x10f7
0x49830: lea rsi,[rip+0xfffffffffffffb79] # 493b0 <camlTest.fun_594>
0x49837: mov QWORD PTR [rdi],rsi
0x4983a: movabs rsi,0x100000000000005
0x49844: mov QWORD PTR [rdi+0x8],rsi
0x49848: mov QWORD PTR [rdi+0x10],rax
0x4984c: mov QWORD PTR [rdi+0x18],rbx
0x49850: mov edi,0x1
camlTest_fun_594는 다음과 같이 디스어셈블됩니다.
0x493b0: mov rsi,rax
0x493b3: mov rdi,QWORD PTR [rbx+0x10]
0x493b7: mov rax,QWORD PTR [rbx+0x18]
0x493bb: mov rbx,QWORD PTR [rax+0x18]
0x493bf: mov rax,QWORD PTR [rax+0x20]
0x493c3: mov rax,QWORD PTR [rax+0x18]
0x493c7: jmp 49320 <camlTest.gcd4_286>
그리고 마지막 호출: let final = tmp3 (int_of_string Sys.argv.(4)) 은 다음과 같이 메모리를 초기화합니다.
0x498de: mov rsi,rax
0x498e1: mov rax,QWORD PTR [rsp+0x8]
0x498e6: mov rdi,QWORD PTR [rax+0x10]
0x498ea: mov rax,QWORD PTR [rax+0x18]
0x498ee: mov rbx,QWORD PTR [rax+0x18]
0x498f2: mov rax,QWORD PTR [rax+0x20]
0x498f6: mov rax,QWORD PTR [rax+0x18]
0x498fa: call 49320 <camlTest.gcd4_286>
gcd4 함수는 미리 .data 섹션에 그 구조가 초기화되어 있습니다.
let tmpN = tmp(N-1) ... 형태의 호출이 발생할 때 마다 런타임에 생성한 Closure 구조와 동일한 구조를 가지고 있습니다.
.data:0x00000000000D5130 camlTest_57_gcd4 dq 0FF7h ; header
.data:0x00000000000D5138 dq offset caml_curry4 ; curry_helper
.data:0x00000000000D5140 dq 400000000000007h ; arity
.data:0x00000000000D5148 dq offset camlTest_gcd4_286; fn
전체적으로 정리해보면 다음과 같이 도식화할 수 있습니다.

위 관찰에서 다음과 같은 특징을 확인할 수 있었습니다.
gcd4함수의 Arity는 4이지만, 1부터 3까지의 Arity에 매칭되는 익명의 Wrapper Symbol이 존재합니다. (fun_594,fun_582,fun_573)- 인자 전달 방식
- 1st - (Arity)-th: 직접 적용될 인자를 전달합니다.
- (Arity + 1)-th: Closure pointer를 전달받습니다. 적용해야 할 나머지 인자는 Closure의 Linked List를 순회하면서 얻어옵니다.
- 역할
- Wrapper Symbol은 마지막에
call이 아닌,jmp를 통해gcd4로 이동합니다. 해당 Wrapper는 Arity에 맞게 레지스터 및 메모리로부터 인자를 세팅하는 역할만 수행합니다. - 메인 로직을 수행하는 함수 호출은 마지막
call camlTest_gcd4_286에서 발생합니다. 그 이전까지는 메모리에 Wrapper를 사용하여 Closure 구조를 생성하고 저장하며, 실제 호출은 일어나지 않습니다.
- Wrapper Symbol은 마지막에
- 인자 전달 방식
.code 영역에 존재하는 실제 함수 코드는 함수 인자가 모두 정해진 시점에 호출되며, 이를 통해 Lazy Evaluation이 가능하다는 점을 알 수 있습니다.
Function-as-a-Value
컴파일 타임에 함수의 Arity가 정해지지 않는 다면 어떨까요? 예를 들어, List.map는 ( 'a -> 'b ) -> 'a list -> 'b list 타입을 가집니다.
타입은 명확하지만, Type variable인 'a 와 'b는 다양한 Arity를 가질 수 있습니다.
int list에 함수를 적용할 수도 있고,(int -> int) list에 함수를 적용할 수도 있고,(int -> int -> int) list에 함수를 적용할 수도 있습니다. (그리고 계속…)
OCaml 런타임은 이러한 상황을 처리하기 위해 caml_curryN_K, caml_curryN_K_app, caml_applyN과 같은 함수를 사용하여, 함수가 Curry되고, Closure가 생성되고, 올바른 함수가 호출되도록 관리합니다.
아까와 같은 gcd4 함수를 사용하되, 고차 함수를 접목시켜 보겠습니다.
expand함수는fn배열의 각 함수마다arg배열의 각 요소를 적용하여fn과arg의 모든 조합을 계산하는 함수입니다.gcd4함수를expand함수에 전달하여 길이 4인step1배열을 생성합니다. 이에 반복적으로expand함수를 적용하여stepN배열에는 총 개의 조합이 포함됩니다. 이 예시에서,expand함수에 적용되는 ('a -> 'b) 는((int -> int) -> (int))가 될 수도 있고,((int -> int -> int) -> (int -> int))가 될 수도 있습니다.
let rec gcd a b = if b = 0 then a else gcd b (a mod b) [@@inline never]
let gcd4 x y z w = gcd x (gcd y (gcd z w)) [@@inline never] (* i -> i -> i -> i => i *)
let expand (fn: ('a -> 'b) array) (arg: 'a array) : 'b array =
Array.init (
(Array.length fn) * (Array.length arg)
)(
(fun i -> fn.(i / Array.length arg) arg.(i mod Array.length arg))
)
let () =
let input = Array.map (fun n -> int_of_string n) (Array.sub Sys.argv 1 ((Array.length Sys.argv) - 1)) in
let step1 = Array.map gcd4 input in
let step2 = expand step1 input in
let step3 = expand step2 input in
let step4 = expand step3 input in
Array.iteri (fun i result ->
Printf.printf "Combination %3d: %d\n" i result
) step4
caml_curryN_K, caml_curryN_K_app, caml_curryN의 관계는 주석에서 잘 드러나 있습니다. OCaml Internal에서 caml_curryN의 코드를 생성하는 부분을 살펴보면, 다음과 같은 주석이 존재합니다.
(* Generate currying functions:
(defun caml_curryN (arg clos)
(alloc HDR caml_curryN_1 <arity (N-1)> caml_curry_N_1_app arg clos))
(defun caml_curryN_1 (arg clos)
(alloc HDR caml_curryN_2 <arity (N-2)> caml_curry_N_2_app arg clos))
...
(defun caml_curryN_N-1 (arg clos)
(let (closN-2 clos.vars[1]
closN-3 closN-2.vars[1]
...
clos1 clos2.vars[1]
clos clos1.vars[1])
(app clos.direct
clos1.vars[0] ... closN-2.vars[0] clos.vars[0] arg clos)))
Special "shortcut" functions are also generated to handle the
case where a partially applied function is applied to all remaining
arguments in one go. For instance:
(defun caml_curry_N_1_app (arg2 ... argN clos)
(let clos' clos.vars[1]
(app clos'.direct clos.vars[0] arg2 ... argN clos')))
Those shortcuts may lead to a quadratic number of application
primitives being generated in the worst case, which resulted in
linking time blowup in practice (PR#5933), so we only generate and
use them when below a fixed arity 'max_arity_optimized'.
*)
- 은 함수의 Arity입니다.
- 는 한번에 적용되는 인자의 개수입니다.
graph LR
subgraph Arity3
C30[caml_curry3] --> C31[caml_curry3_1]
C30 --> C31A[caml_curry3_1_app]
C31 --> C32[caml_curry3_2]
end
subgraph Arity4
C40[caml_curry4] --> C41[caml_curry4_1]
C40 --> C41A[caml_curry4_1_app]
C41A -..-> Arity3
C41 --> C42[caml_curry4_2]
C41 --> C42A[caml_curry4_2_app]
C42 --> C43[caml_curry4_3]
end
각 함수를 살펴보면서 역할을 알아봅시다.
caml_curry4
<caml_curry4>:
sub r15,0x30
cmp r15,QWORD PTR [r14]
,-- jb <caml_curry4+0x44>
,--|-> lea rdi,[r15+0x8]
| | mov QWORD PTR [rdi-0x8],0x14f7
| | lea rsi,[rip+0x64] # <caml_curry4_1>
| | mov QWORD PTR [rdi],rsi
| | movabs rsi,0x300000000000007
| | mov QWORD PTR [rdi+0x8],rsi
| | lea rsi,[rip+0x1c] # <caml_curry4_1_app>
| | mov QWORD PTR [rdi+0x10],rsi
| | mov QWORD PTR [rdi+0x18],rax
| | mov QWORD PTR [rdi+0x20],rbx
| | mov rax,rdi
| | ret
| '-> call <caml_call_gc>
'----- jmp <caml_curry4+0x9>
Note
r15와 r14는 앞서 언급했듯, 메모리 관리에 사용하는 레지스터입니다. 함수의 호출 기작과 큰 관련이 없으므로 분석을 생략하겠습니다.
2개의 인자를 받고, 각각 다음과 같습니다.
- 1번째 인자
arg: 최종적으로 호출될 함수에 적용될 인자입니다. - 2번째 인자
clos: Closure pointer으로, Caller function의 Closure가 저장되어 있습니다.
함수의 첫 부분에서 필요한 경우 메모리를 할당하고 Garbage collection을 수행합니다. 할당한 메모리 공간에 Closure 구조체를 생성하고 있습니다.
rdi가 Closure structure를 가리키게 되며, 다음의 정보를 메모리에 작성합니다.
Offset to rdi | Value | Info |
|---|---|---|
-0x8 | 0x14F7 | Header |
0x0 | caml_curry4_1 | Function to be called by currying |
0x8 | 0x300000000000007 | Arity and Context info |
0x10 | caml_curry4_1_app | Function to be applied directly |
0x18 | rax | 1st argument (Argument pass to gcd4 function) |
0x20 | rbx | 2nd argument (Pointer to caller’s closure) |
rbx에 Caller function의 Closure가 저장된다는 점이 중요합니다. (gdb에서 중단점을 걸고 확인해 봅시다.)
caml_curry4_1
<caml_curry4_1>:
sub r15,0x30
cmp r15,QWORD PTR [r14]
,-- jb <caml_curry4_1+0x44>
,--|-> lea rdi,[r15+0x8]
| | mov QWORD PTR [rdi-0x8],0x14f7
| | lea rsi,[rip+0x64] # <caml_curry4_2>
| | mov QWORD PTR [rdi],rsi
| | movabs rsi,0x200000000000007
| | mov QWORD PTR [rdi+0x8],rsi
| | lea rsi,[rip+0x1c] # <caml_curry4_2_app>
| | mov QWORD PTR [rdi+0x10],rsi
| | mov QWORD PTR [rdi+0x18],rax
| | mov QWORD PTR [rdi+0x20],rbx
| | mov rax,rdi
| | ret
| '-> call <caml_call_gc>
'----- jmp <caml_curry4_1+0x9>
caml_curry4_1_app
<caml_curry4_1_app>:
mov rcx,rax
mov r8,rbx
mov r9,rdi
cmp r15,QWORD PTR [r14]
,-- jbe <caml_curry4_1_app+0x26>
,--|-> mov rdx,QWORD PTR [rsi+0x20]
| | mov rax,QWORD PTR [rsi+0x18]
| | mov r12,QWORD PTR [rdx+0x10]
| | mov rbx,rcx
| | mov rdi,r8
| | mov rsi,r9
| | jmp r12
| '-> call <caml_call_gc>
'----- jmp <caml_curry4_1_app+0xe>
caml_curry4_3
<caml_curry4_3>:
mov rsi,rax
cmp r15,QWORD PTR [r14]
,-- jbe <caml_curry4_3+0x26>
,--|-> mov rax,QWORD PTR [rbx+0x18]
| | mov rcx,QWORD PTR [rax+0x20]
| | mov rdx,QWORD PTR [rcx+0x20]
| | mov rdi,QWORD PTR [rbx+0x10]
| | mov rbx,QWORD PTR [rax+0x18]
| | mov rax,QWORD PTR [rcx+0x18]
| | mov rcx,QWORD PTR [rdx+0x10]
| | jmp rcx
| '-> call <caml_call_gc>
'----- jmp <caml_curry4_3+0x8>
caml_apply4
<caml_apply4>:
lea r10,[rsp-0x158]
cmp r10,QWORD PTR [r14+0x28]
,----- jb <caml_apply4+0x6c>
,--|----> sub rsp,0x18
| | mov rcx,QWORD PTR [rdx+0x8]
| | sar rcx,0x38
| | cmp rcx,0x4
| | ,-- jne <caml_apply4+0x2c>
| | | mov rcx,QWORD PTR [rdx+0x10]
| | | add rsp,0x18
| | | jmp rcx
| | | xchg ax,ax
| | '-> mov QWORD PTR [rsp+0x10],rsi
| | mov QWORD PTR [rsp+0x8],rdi
| | mov QWORD PTR [rsp],rbx
| | mov rdi,QWORD PTR [rdx]
| | mov rbx,rdx
| | call rdi
| | mov rbx,rax
| | mov rdi,QWORD PTR [rbx]
| | mov rax,QWORD PTR [rsp]
| | call rdi
| | mov rbx,rax
| | mov rdi,QWORD PTR [rbx]
| | mov rax,QWORD PTR [rsp+0x8]
| | call rdi
| | mov rbx,rax
| | mov rdi,QWORD PTR [rbx]
| | mov rax,QWORD PTR [rsp+0x10]
| | add rsp,0x18
| | jmp rdi
| '----> push 0x24
| call <caml_call_realloc_stack>
| pop r10
'-------- jmp <caml_apply4+0xe>
(Writing in progress…)