@@ -153,15 +153,26 @@ Turning to compilation, let's start small by supposing we have a
153153single, pre-defined function and we add to the language the ability to
154154call 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]
173184instruction, which can be given a label, designating which function to
174185call.
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
191194The 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
198197The @racket[Call] instruction advances @racket['rsp ] to the next word
199198of 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
216215The 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]
218217advances (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
278266It'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
292290If 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
311309So 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.
349349After the instructions for the body, a @racket[(Ret)] instruction is
350350emitted 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-
363352What about functions that take zero or more arguments? That's easy,
364353just 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
376367arguments 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
434454It relies on a helper @racket[compile-defines] for compiling each
0 commit comments