Skip to content

Commit 0477f9b

Browse files
committed
New Iniquity notes
Things we had to change: * We no longer use rsp as a base pointer, so I had to rework the explanation of function calls and where the arguments are as they related to rsp * Because we push things on the stack we have to have to track alignment differently New things we had to address: * Parity for alignment within the function * padding the stack for alignment before a call
1 parent ac6868b commit 0477f9b

2 files changed

Lines changed: 143 additions & 123 deletions

File tree

langs/iniquity/compile.rkt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
;; type CEnv = [Listof Variable]
1515

16-
;; Expr -> Asm
16+
;; Prog -> Asm
1717
(define (compile p)
1818
(match p
1919
[(Prog ds e)

www/notes/iniquity.scrbl

Lines changed: 142 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,26 @@ Turning to compilation, let's start small by supposing we have a
153153
single, pre-defined function and we add to the language the ability to
154154
call this function.
155155

156-
A function in assembly has an entry point (a label), followed by a
157-
sequence of instruction, ending with the @tt{'ret} instruction. As a
158-
convention, we will pass all arguments to a function on the stack.
156+
A function in assembly has an entry point (a label), followed by a sequence of
157+
instruction, ending with the @tt{'ret} instruction. A very common approach for
158+
passing arguments to a function, which we will use here, is to pass arguments
159+
via the stack, this way you don't have to worry as much about which registers
160+
may or may not be used (at the cost of performance).
159161

160-
So here is @tt{Asm} representing a single function named @tt{double}
162+
163+
@#reader scribble/comment-reader
164+
(racketblock
165+
(seq (Push rax) ; argument is now at rsp
166+
(Call 'double)
167+
(Add 'rax 1)) ; rax now holds 11
168+
)
169+
170+
So far, so good! Now we can look at what the code for the @tt{double} might
171+
look like:
161172

162173
@racketblock[
163174
(seq (Label 'double)
164-
(Mov 'rax (Offset 'rsp -1))
175+
(Mov (Offset 'rsp 0) 5)
165176
(Add 'rax 'rax)
166177
(Ret))
167178
]
@@ -173,27 +184,15 @@ The @racket[Ret] instruction works in concert with the @racket[Call]
173184
instruction, which can be given a label, designating which function to
174185
call.
175186

176-
So if we wanted to call @racket[double] with an argument of 5, we'd
177-
first need to write 5 in to the approrpriate spot in the stack, then
178-
issue the @racket[(Call 'double)] instruction.
187+
So if we wanted to call @racket[double] with an argument of 5, we'd push 5 on
188+
the stack, then issue the @racket[(Call 'double)] instruction.
179189

180-
Since the @tt{double} code is reading from offset -1 from
181-
@racket['rsp], it is tempting to assume this is where you should write
182-
the argument:
183-
184-
@#reader scribble/comment-reader
185-
(racketblock
186-
(seq (Mov (Offset rsp -1) 5)
187-
(Call 'double)
188-
(Add 'rax 1)) ; rax now holds 11
189-
)
190+
The @tt{double} code is reading from offset 0 from @racket['rsp], @emph{seems}
191+
to make sense, since we are pushing the argument right before executing the
192+
@racket[Call] instruction.
190193

191194
The problem is here is that the @racket[Call] instruction works by
192-
modifying the @racket['rsp] register.
193-
194-
Remember how @racket['rsp] points to an ``occupied'' memory location
195-
and we said we just leave whatever is there alone? We can now explain
196-
what's going on.
195+
modifying the @racket['rsp] register!
197196

198197
The @racket[Call] instruction advances @racket['rsp] to the next word
199198
of memory and writes the location of the instruction that occurs after
@@ -214,79 +213,78 @@ So calls and returns in assembly are really just shorthand for:
214213
]
215214

216215
The problem with the function call we wrote above is that we put the
217-
argument in @racket[(Offset 'rsp -1)], but then the @racket[Call]
216+
argument in @racket[(Offset 'rsp 0)], but then the @racket[Call]
218217
advances (by decrementing) the @racket['rsp] register and writes the
219-
return point in @racket[(Offset 'rsp 0)], but that's exactly where we
220-
had put the argument!
221-
222-
The solution then, is to put the argument at index -2 from the
223-
caller's perspective. When the call is made, it will be at index -1
224-
from the function's perspective:
225-
226-
@#reader scribble/comment-reader
227-
(racketblock
228-
(seq (Mov (Offset 'rsp -2) 5)
229-
(Call 'double)
230-
(Add 'rax 1)) ; rax now holds 11
231-
)
218+
return point in @racket[(Offset 'rsp 0)], so now the relative offset
219+
from @racket['rsp] would be 8, not 0!
232220

233-
Now that we have seen how to make a call and return in assembly, we
234-
can tackle code generation for a function call @racket[(double _e)] in
235-
our language.
221+
The solution then, is to make sure that the function knows that
222+
it's arguments have been ``shifted'' by one slot.
236223

237224
@racketblock[
238-
;; Expr CEnv -> Asm
239-
(define (compile-call-double e0 c)
240-
(seq (compile-e e0 c)
241-
(Mov (Offset 'rsp -2) 'rax) ; place result of e0 in stack
242-
(Call 'double)))
225+
(seq (Label 'double)
226+
(Mov (Offset 'rsp 8) 5)
227+
(Add 'rax 'rax)
228+
(Ret))
243229
]
244230

245-
This will work if the program consists only of a call to
246-
@racket[double], however it doesn't work in general.
247-
248-
To see the problem, notice how the call code always uses the index -2
249-
for the first argument and index -1 will hold the return pointer when
250-
the call is made. But what if those spots are occuppied on the
251-
stack!? The problem is that we've always calculated stack offsets
252-
statically and never mutated @racket['rsp]. But @racket[Call]
253-
expects @racket['rsp] to be pointing to the top of the stack.
231+
We're not out of the woods yet. What we've described above @emph{would} work,
232+
on an idealized machine. However, the System V x86_64 calling convention adds
233+
one more constraint on us: @tt{rsp} @emph{must} be aligned to 16-bytes when a
234+
function call is performed. This requires us to do some calculating before we
235+
can determine whether we need to pad that stack. This requires us to know how
236+
many things are currently on our stack, luckily we already have an environment
237+
on hand, which provides this information. So for @racket[double], it would
238+
look like the following:
254239

255-
The solution is to emit code that will adjust @racket['rsp] to the top
256-
of (our statically calculated) stack. How much does @racket['rsp]
257-
need to change? It needs to be decremented by the number of items in
258-
the static environment, @racket[c]. We can adjust @racket['rsp], make
259-
the call, but after the call returns, we can adjust @racket['rsp] back
260-
to where it was before the call.
261-
262-
The code is:
263240

264241
@#reader scribble/comment-reader
265242
(racketblock
266-
;; Expr CEnv -> Asm
267-
(define (compile-call-double e0 c)
268-
(let ((h (* 8 (length c))))
269-
(seq (compile-e e0 c)
270-
(Sub 'rsp h)
271-
(Mov (Offset 'rsp -2) rax) ; place result of e0 in stack
272-
(Call 'double)
273-
(Add 'rsp h))))
274-
)
243+
(define (compile-double-call e c)
244+
245+
; determine whether stack is 16-byte aligned
246+
; based on the number of things on the stack + our argument
247+
(if (even? (add1 (length c)))
248+
249+
; Stack will be 16-byte aligned:
250+
(seq (compile-es e c) ; generate code for the argument
251+
(Call 'double)
252+
(Add rsp 8)) ; pop argument
253+
254+
; Stack will _not_ be 16-byte aligned
255+
; We need to pad the stack
256+
(seq (Sub rsp 8) ; pad stack
257+
(compile-es es (cons #f c)) ; generate code for the argument
258+
; taking the pad into account
259+
(Call 'double)
260+
(Add rsp 16))))) ; pop args and pad
261+
))
275262

276-
This makes calls work in any stack context.
263+
This will work if the program consists only of a call to
264+
@racket[double], however it doesn't work in general.
277265

278266
It's easy to generalize this code to call any given function name:
279267

280268
@#reader scribble/comment-reader
281269
(racketblock
282-
;; Id Expr CEnv -> Asm
283-
(define (compile-call f e0 c)
284-
(let ((h (* 8 (length c))))
285-
(seq (compile-e e0 c)
286-
(Sub 'rsp h)
287-
(Mov (Offset 'rsp -2) 'rax)
288-
(Call f)
289-
(Add 'rsp h))))
270+
(define (compile-double-call e c)
271+
272+
; determine whether stack is 16-byte aligned
273+
; based on the number of things on the stack + our argument
274+
(if (even? (add1 (length c)))
275+
276+
; Stack will be 16-byte aligned:
277+
(seq (compile-es e c) ; generate code for the argument
278+
(Call 'double)
279+
(Add rsp 8)) ; pop argument
280+
281+
; Stack will _not_ be 16-byte aligned
282+
; We need to pad the stack
283+
(seq (Sub rsp 8) ; pad stack
284+
(compile-es es (cons #f c)) ; generate code for the argument
285+
; taking the pad into account
286+
(Call 'double)
287+
(Add rsp 16)))) ; pop args and pad
290288
)
291289

292290
If we want accept any number of arguments, we have to do a little more
@@ -304,27 +302,29 @@ expressions and saving the results on the stack:
304302
['() '()]
305303
[(cons e es)
306304
(seq (compile-e e c)
307-
(Mov (Offset 'rsp (- (add1 (length c)))) 'rax)
305+
(Push rax)
308306
(compile-es es (cons #f c)))]))
309307
)
310308

311309
So to compile a call with any number of arguments:
312310

313311
@#reader scribble/comment-reader
314312
(racketblock
315-
;; Id (Listof Expr) CEnv -> Asm
316-
(define (compile-call f es c)
317-
(let ((h (* 8 (length c))))
318-
(seq (compile-es es (cons #f c))
319-
(Sub 'rsp h)
320-
(Call f)
321-
(Add 'rsp h))))
313+
(define (compile-app f es c)
314+
(if (even? (+ (length es) (length c)))
315+
(seq (compile-es es c)
316+
(Call f)
317+
(Add rsp (* 8 (length es)))) ; pop args
318+
(seq (Sub rsp 8) ; adjust stack
319+
(compile-es es (cons #f c))
320+
(Call f)
321+
(Add rsp (* 8 (add1 (length es))))))) ; pop args and pad
322322
)
323323

324-
Notice that we call @racket[compile-es] in an extended static
325-
environment, that has one addition slot used. This will bump the
326-
location of all the argument results by one, leaving the first slot
327-
available for the return pointer!
324+
Notice that in the `else' branch we call @racket[compile-es] in an extended
325+
static environment, that has one addition slot used. This will bump the
326+
location of all the argument results by one, which is necessary for the 16-byte
327+
alignment.
328328

329329
@section[#:tag-prefix "iniquity"]{Compiling a Function Definition}
330330

@@ -349,32 +349,39 @@ value.
349349
After the instructions for the body, a @racket[(Ret)] instruction is
350350
emitted so that control transfers back to the caller.
351351

352-
So the code for compiling a function definition is:
353-
354-
@#reader scribble/comment-reader
355-
(racketblock
356-
;; Id Id Expr -> Asm
357-
(define (compile-define f x e0)
358-
(seq (Label f)
359-
(compile-e e0 (list x))
360-
(Ret)))
361-
)
362-
363352
What about functions that take zero or more arguments? That's easy,
364353
just compile the body in an appropriate static environment.
365354

366355
@#reader scribble/comment-reader
367356
(racketblock
368-
;; Id (Listof Id) Expr -> Asm
369-
(define (compile-define f xs e0)
370-
(seq (Label f)
371-
(compile-e e0 (reverse xs))
372-
(Ret)))
357+
;; Defn -> Asm
358+
(define (compile-define d)
359+
(match d
360+
[(Defn f xs e)
361+
(seq (Label f)
362+
(compile-e e (parity (cons #f (reverse xs))))
363+
(Ret))]))
373364
)
374365

375366
(Note that we reverse the parameter list due to the order in which
376367
arguments are added to the stack.)
377368

369+
The @racket[parity] function is there to manage alignment appropriately.
370+
Because we know that the @racket[Call] instruction must be executed with
371+
@racket['rsp] being 16-byte aligned, and that @racket[Call] @emph{pushes} the
372+
return pointer on the stack, we have to make sure that the environment
373+
accurately portrays the stack as @emph{not} 16-byte aligned at the beginning of
374+
the function's code. To do this we add a dummy value to the @emph{end} of the
375+
environment if it has an even number of items (even would imply that we are
376+
16-byte aligned, when we know that we are not).
377+
378+
@#reader scribble/comment-reader
379+
(racketblock
380+
(define (parity c)
381+
(if (even? (length c))
382+
(append c (list #f))
383+
c))
384+
)
378385

379386
@section[#:tag-prefix "iniquity"]{On Names and Labels}
380387

@@ -401,18 +408,23 @@ Using this function, we can touch up our code:
401408
@#reader scribble/comment-reader
402409
(racketblock
403410
;; Id (Listof Expr) CEnv -> Asm
404-
(define (compile-call f es c)
405-
(let ((h (* 8 (length c))))
406-
(seq (compile-es es (cons #f c))
407-
(Sub 'rsp h)
408-
(Call (symbol->label f))
409-
(Add 'rsp h))))
410-
411-
;; Id (Listof Id) Expr -> Asm
412-
(define (compile-define f xs e0)
413-
(seq (Label (symbol->label f))
414-
(compile-e e0 (reverse xs))
415-
(Ret)))
411+
(define (compile-app f es c)
412+
(if (even? (+ (length es) (length c)))
413+
(seq (compile-es es c)
414+
(Call (symbol->label f))
415+
(Add rsp (* 8 (length es)))) ; pop args
416+
(seq (Sub rsp 8) ; adjust stack
417+
(compile-es es (cons #f c))
418+
(Call (symbol->label f))
419+
(Add rsp (* 8 (add1 (length es))))))) ; pop args and pad
420+
421+
;; Defn -> Asm
422+
(define (compile-define d)
423+
(match d
424+
[(Defn f xs e)
425+
(seq (Label (symbol->label f))
426+
(compile-e e (parity (cons #f (reverse xs))))
427+
(Ret))]))
416428
)
417429

418430

@@ -427,8 +439,16 @@ complete program:
427439
(define (compile p)
428440
(match p
429441
[(Prog ds e)
430-
(seq (compile-entry e)
431-
(compile-defines ds))]))
442+
(prog (Extern 'peek_byte)
443+
(Extern 'read_byte)
444+
(Extern 'write_byte)
445+
(Extern 'raise_error)
446+
(Label 'entry)
447+
(Mov rbx rdi) ; recv heap pointer
448+
(compile-e e '(#f))
449+
(Mov rdx rbx) ; return heap pointer in second return register
450+
(Ret)
451+
(compile-defines ds))]))
432452
)
433453

434454
It relies on a helper @racket[compile-defines] for compiling each

0 commit comments

Comments
 (0)