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이 저장됨)를 가질 수 있습니다.

블럭의 헤더는 다음과 같이 정의되어 있으며, 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

/* 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

참조되는 포인터 위치를 확인해 봅시다.

AddressBytesInfo
0xBC280FC07 00 00 00 00 00 00Header
0xBC28848 65 6C 6C 6F00 00 02String

Word-size align을 맞추기 위해 가장 마지막 바이트에 NULL Count를 넣어 문자열의 크기를 나타내고 있습니다. String Tag는 Header에서 0xFC로 표현됩니다.

Floating-point

let my_float = 3.1415

IDA에서 확인했을 때, 다음과 같이 초기화됩니다.

caml_initialize(&camlTest[3], &camlTest_3); // 0xBC180

참조되는 포인터 위치를 확인해 봅시다.

AddressBytesInfo
0xBC178FD78 00 00 00 00 00 00Header
0xBC1806F 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에 주의하며 살펴봅시다.

Nested Tuple

Lists

OCaml에서 list의 패턴 매칭을 시도할 때 hd::tl 형태로 표현됩니다. 마찬가지로 list가 재귀적으로 정의된 데이터 구조이기 때문입니다.

let my_list = [10; 20; 30; 40]

메모리에 다음과 같이 나타납니다. 각각의 블럭은 길이 2 (hdtl 포인터)로 구성되어 있습니다.

  • 본 예시에서는 int만을 사용하므로, hd 부분이 Tagged Integer로 표현됩니다. (int가 아닌 타입을 사용한다면, 블럭에 대한 포인터로 표현될 것입니다.)
  • tl 부분은 다음 블럭에 대한 포인터로 표현됩니다. 리스트의 끝은 1 == (0 << 1 | 1)으로 표현됩니다. NULL pointer가 아닌, Tagged Integer로 표현된다는 점에 주의합시다.

Nested Tuple

How ‘Functions’ are represented in OCaml

본 문서의 핵심

Warning

본 문서를 이해하기 위해, Currying, Closure 등과 같은 프로그래밍 언어 이론에 대한 간단한 지식이 필요합니다.

일반적으로 절차지향적 프로그래밍 언어에서 사용하는 함수는 함수를 반환하지 않습니다. 예를 들어, 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 PointerAllocation 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

전체적으로 정리해보면 다음과 같이 도식화할 수 있습니다.

Static Arity Function Call

위 관찰에서 다음과 같은 특징을 확인할 수 있었습니다.

  • 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 구조를 생성하고 저장하며, 실제 호출은 일어나지 않습니다.

.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 배열의 각 요소를 적용하여 fnarg의 모든 조합을 계산하는 함수입니다.
  • gcd4 함수를 expand 함수에 전달하여 길이 4인 step1 배열을 생성합니다. 이에 반복적으로 expand 함수를 적용하여 stepN 배열에는 총 4N4^N 개의 조합이 포함됩니다. 이 예시에서, 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'.
*)
  • NN은 함수의 Arity입니다.
  • KK는 한번에 적용되는 인자의 개수입니다.
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

r15r14는 앞서 언급했듯, 메모리 관리에 사용하는 레지스터입니다. 함수의 호출 기작과 큰 관련이 없으므로 분석을 생략하겠습니다.

2개의 인자를 받고, 각각 다음과 같습니다.

  • 1번째 인자 arg: 최종적으로 호출될 함수에 적용될 인자입니다.
  • 2번째 인자 clos: Closure pointer으로, Caller function의 Closure가 저장되어 있습니다.

함수의 첫 부분에서 필요한 경우 메모리를 할당하고 Garbage collection을 수행합니다. 할당한 메모리 공간에 Closure 구조체를 생성하고 있습니다. rdi가 Closure structure를 가리키게 되며, 다음의 정보를 메모리에 작성합니다.

Offset to rdiValueInfo
-0x80x14F7Header
0x0caml_curry4_1Function to be called by currying
0x80x300000000000007Arity and Context info
0x10caml_curry4_1_appFunction to be applied directly
0x18rax1st argument (Argument pass to gcd4 function)
0x20rbx2nd 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…)