Scott Knight
6 min read

Categories

Tags

A common question that comes up when people start Swift development is what’s the difference between a struct and a class? The standard answer is structs are value types and classes are reference types. The Swift Programming Language book has a whole section reviewing this concept in more detail. From a reverse engineering perspective I always find it interesting to dive under the hood and see how the compiler actually handles the different concepts from high level languages. This post presents a very simple example of a struct and class in Swift and how the compiler deals with them.

Let’s start off with the example Swift code.

class CoordClass {
    let x: Int
    let y: Int
    let z: Int
    
    init(x: Int, y: Int, z: Int) {
        self.x = x
        self.y = y
        self.z = z
    }
}

struct CoordStruct {
    let x: Int
    let y: Int
    let z: Int
}

func CoordClassSum(_ c: CoordClass) -> Int {
    return c.x + c.y + c.z;
}

func CoordStructSum(_ c: CoordStruct) -> Int {
    return c.x + c.y + c.z;
}

let c1 = CoordClass(x:0x40, y:0x50, z:0x60)
let c2 = CoordStruct(x:0x10, y:0x20, z:0x30)

print("CoordClassSum  = \(CoordClassSum(c1))\n")
print("CoordStructSum = \(CoordStructSum(c2))\n")

You can copy and paste this code into an Xcode playground if you want to play around with it. In both cases we define a coordinate data type composed of a x, y and z value. Then we define a function for the data type that will sum all the values together. We’ll start by taking a look at the disassembly of the call to CoordClassSum function.

000000010000226c 488B3D05645800         mov        rdi, qword [c1]              ; argument #1 for method CoordClassSum, c1
0000000100002273 E848030000             call       CoordClassSum                

You can clearly see that the argument to the CoordClassSum function is the address of the c1 object we created. This is where the term “reference type” comes from. We’re not passing the x, y and z values directly but rather just a reference to the object that was created. Next lets look at the actual CoordClassSum function itself.

00000001000025c0 55                     push       rbp                          ; CODE XREF=_main+755
00000001000025c1 4889E5                 mov        rbp, rsp
00000001000025c4 48C745F800000000       mov        qword [rbp+var_8], 0x0
00000001000025cc 48897DF8               mov        qword [rbp+var_8], rdi
00000001000025d0 488B4710               mov        rax, qword [rdi+0x10]        ; Set rax = c1.x
00000001000025d4 48034718               add        rax, qword [rdi+0x18]        ; Add c1.y to rax
00000001000025d8 0F90C1                 seto       cl
00000001000025db 48897DF0               mov        qword [rbp+var_10], rdi
00000001000025df 488945E8               mov        qword [rbp+var_18], rax      ; Save the c1.x + c1.y result into a temp var
00000001000025e3 884DE7                 mov        byte [rbp+var_19], cl
00000001000025e6 701E                   jo         loc_100002606                ; Check for overflow

00000001000025e8 488B45E8               mov        rax, qword [rbp+var_18]      ; Set rax to the saved result of c1.x + c1.y
00000001000025ec 488B4DF0               mov        rcx, qword [rbp+var_10]      
00000001000025f0 48034120               add        rax, qword [rcx+0x20]        ; Add c1.z to rax
00000001000025f4 0F90C2                 seto       dl
00000001000025f7 488945D8               mov        qword [rbp+var_28], rax      ; Save final sum onto the stack
00000001000025fb 8855D7                 mov        byte [rbp+var_29], dl
00000001000025fe 7008                   jo         loc_100002608                ; Check for overflow

0000000100002600 488B45D8               mov        rax, qword [rbp+var_28]      ; Return the final value of c1.x + c1.y + c1.z
0000000100002604 5D                     pop        rbp
0000000100002605 C3                     ret

In the case of the class you can see how all access to the x, y and z member variables are through the object reference that was originally passed in. The rdi register is holding the reference to the object and we access member variables with instructions like mov rax, qword [rdi+0x10]. Now lets take a look at the call to the CoordStructSum.

00000001000020ab 488B3DAE655800         mov        rdi, qword [c2.x]            ; argument #1 for method CoordStructSum, x
00000001000020b2 488B35AF655800         mov        rsi, qword [c2.y]            ; argument #2 for method CoordStructSum, y
00000001000020b9 488B15B0655800         mov        rdx, qword [c2.z]            ; argument #3 for method CoordStructSum, z
00000001000020c0 E8AB040000             call       CoordStructSum               

Looking at this disassembly you can already see the difference. Even though we have the CoordStructSum function defined almost identical to the CoordClassSum function, behind the scenes the compiler is going to pass the struct by its individual values. This means that there are really three separate arguements passed to this function. Now lets look at the implementation of CoordStructSum.

0000000100002570 55                     push       rbp                          ; CODE XREF=_main+320
0000000100002571 4889E5                 mov        rbp, rsp
0000000100002574 48897DE8               mov        qword [rbp+var_18], rdi      ; Save c2.x to the stack
0000000100002578 488975F0               mov        qword [rbp+var_10], rsi      ; Save c2.y to the stack
000000010000257c 488955F8               mov        qword [rbp+var_8], rdx       ; Save c2.z to the stack
0000000100002580 4801F7                 add        rdi, rsi                     ; Add c2.x and c2.y
0000000100002583 0F90C0                 seto       al
0000000100002586 48897DE0               mov        qword [rbp+var_20], rdi      ; Save the c2.x + c2.y result into a temp var
000000010000258a 488955D8               mov        qword [rbp+var_28], rdx
000000010000258e 8845D7                 mov        byte [rbp+var_29], al
0000000100002591 701D                   jo         loc_1000025b0                ; Check for overflow

0000000100002593 488B45E0               mov        rax, qword [rbp+var_20]      ; Set rax to the saved result of c2.x + c2.y
0000000100002597 488B4DD8               mov        rcx, qword [rbp+var_28]
000000010000259b 4801C8                 add        rax, rcx                     ; Add c2.z to rax
000000010000259e 0F90C2                 seto       dl
00000001000025a1 488945C8               mov        qword [rbp+var_38], rax      ; Save the final sum onto the stack
00000001000025a5 8855C7                 mov        byte [rbp+var_39], dl
00000001000025a8 7008                   jo         loc_1000025b2                ; Check for overflow

00000001000025aa 488B45C8               mov        rax, qword [rbp+var_38]      ; Return the final value of c2.x + c2.y +c2.z
00000001000025ae 5D                     pop        rbp
00000001000025af C3                     ret

So what’s the lesson to be learned from all of this? Well from a reverse enginering point of view, when reversing swift code, looking at the functions calls and what’s passed in might give some hint to whether the original code was using a struct or a class to implement the functionality. From a developer view I think it’s important to keep in mind that no matter how large your struct is the compiler is going to attempt to copy all the values and pass them in to the function call. In this case, with only three member variables, it doesn’t make much of a difference but in a larger struct with variables that are using larger chunks of memory it could.