---
title: Y86-64 Programming
module: Instruction Set Architecture
moduleNumber: 2
lessonNumber: 5
order: 205
summary: >
  With the encodings fixed, we write real Y86-64 assembly: the .pos, .align, and
  .quad directives, the calling convention borrowed from x86-64, a stack set up by
  hand, and complete programs — an array sum and a branch-free max. We watch the
  assembler turn the listing into the exact byte image the processor will execute,
  and trace the stack across the call.
topics: [Instruction Set Architecture]
sources:
  - book: Bryant & O'Hallaron
    ref: "CS:APP — §4.1 The Y86-64 Instruction Set Architecture"
  - book: Bistriceanu
    ref: "Computer Architecture Notes — §5 CPU Implementation"
---

A Y86-64 program is plain text in a `.ys` file, and the assembler's job is to turn
that text into the byte image from the
[previous lesson](/computer-architecture/instruction-set-architecture/the-y86-64-instruction-set)
laid out at the right addresses in memory. Writing one end to end exercises all of
it at once: directives place code and data, labels become addresses, the
stack has to be set up by hand because there is no operating system underneath, and
`call`/`ret` move the program counter through the stack. This lesson writes a
complete array-sum program, follows it all the way down to the bytes, and closes
with the branch-free conditional idiom that `cmovXX` exists for.

## Directives: placing code and data

Most lines of a `.ys` file are instructions, but a few are **assembler directives** —
commands to the assembler that emit no opcode of their own, only control where
things land.

- `.pos a` sets the **location counter** to address `a`: the next byte the assembler
  emits goes there. A program begins with `.pos 0`, and we use another `.pos` to
  park the stack far from the code.
- `.align k` advances the location counter to the next multiple of `k`, padding with
  zero bytes. Used before data so multi-byte values sit at aligned addresses, exactly
  the [alignment](/computer-architecture/machine-level-x86-64/arrays-structs-and-alignment)
  the hardware prefers.
- `.quad v` emits the 8-byte value `v`, little-endian. It is how we lay down initial
  data — here, the array elements.

> **Definition (Assembler directive).** A line in an assembly file that instructs
> the assembler rather than the processor. Directives like `.pos`, `.align`, and
> `.quad` place code and data and emit constants, but produce no executable opcode.

A label is just a name for "the current location counter value." When the assembler
reaches `sum:` it records the address of the next byte under the name `sum`, and
every `call sum` or `jmp test` elsewhere is resolved to that address.

## The calling convention

Y86-64 the ISA says nothing about how procedures pass arguments; `call` and `ret`
only move the PC. Everything else is **convention**, and Y86-64 programs adopt
x86-64's System V convention wholesale (minus `%r15`, which Y86-64 does not have)
so that compiled C maps straight over:

| Role | Registers |
| --- | --- |
| Argument 1–6 (in order) | `%rdi`, `%rsi`, `%rdx`, `%rcx`, `%r8`, `%r9` |
| Return value | `%rax` |
| Stack pointer | `%rsp` |
| Caller-saved (freely clobbered) | `%rax`, `%rcx`, `%rdx`, `%rsi`, `%rdi`, `%r8`–`%r11` |
| Callee-saved (push/restore if used) | `%rbx`, `%rbp`, `%r12`–`%r14` |

`%rsp` always aims at the top of the stack; `call` pushes the return address there
and `ret` pops it. None of this is enforced by hardware. It is a contract between caller and callee
in exactly the sense the
[first lesson](/computer-architecture/instruction-set-architecture/what-an-isa-is)
gave the word: agree on it, and procedures compiled by different people compose.
Because Y86-64 has no operating system underneath, one more job falls on the
program itself: the very first instruction must establish the stack pointer, by
loading the address of a region reserved with `.pos`.

## A complete program: summing an array

Here is a full Y86-64 program that sums a four-element array and leaves the total in
`%rax`. The structure is the C loop `for (i = n; i != 0; i--) sum += *p++;` written
out in Y86-64, with a `sum` procedure called from a small driver. The array base
and count arrive in `%rdi` and `%rsi`, per the convention.

```asm [array-sum.ys]
.pos 0
irmovq stack, %rsp      # set up stack pointer
irmovq array, %rdi      # %rdi = &array[0]
irmovq $4, %rsi         # %rsi = element count
call sum                # sum(array, 4) -> %rax
halt                    # stop the machine

# long sum(long *p, long count)
sum:
irmovq $8, %r8          # %r8 = 8, the pointer stride
irmovq $1, %r9          # %r9 = 1, the loop decrement
xorq %rax, %rax         # %rax = 0, the running total
jmp test                # enter loop at the test
loop:
mrmovq (%rdi), %r10     # %r10 = *p
addq %r10, %rax         # sum += %r10
addq %r8, %rdi          # p++  (advance 8 bytes)
subq %r9, %rsi          # count--
test:
andq %rsi, %rsi         # set condition codes from count
jne loop                # if count != 0, loop again
ret                     # return, total in %rax

.align 8
array:
.quad 10                # array[0]
.quad 20                # array[1]
.quad 30                # array[2]
.quad 40                # array[3]

.pos 0x200
stack:                  # %rsp starts here, stack grows down
```

$$
% caption: The program's memory map, high addresses at the top. Code fills
% caption: 0x000-0x062, the array's four quads sit at 0x068-0x087 after alignment
% caption: padding, and the stack starts at 0x200 and grows down through the unused
% caption: gap toward the data. Regions are placed by .pos and .align, not by any OS.
\begin{tikzpicture}[font=\footnotesize, >=stealth,
  seg/.style={draw, minimum width=36mm, align=center}]
  \definecolor{acc}{HTML}{2348F2}
  % stack boundary at top
  \draw[thick, acc] (-1.8,3.0) -- (1.8,3.0);
  \node[anchor=east, font=\scriptsize] at (-2.0,3.0) {$\mathtt{0x200}$};
  \node[anchor=west, text=acc] at (2.0,3.0) {stack starts here};
  \draw[->, acc] (0,2.85) -- (0,2.05)
        node[midway, right, font=\scriptsize] {grows down};
  % unused region
  \draw[dashed, black] (-1.8,3.0) -- (-1.8,1.2);
  \draw[dashed, black] (1.8,3.0) -- (1.8,1.2);
  \node[black, font=\scriptsize] at (0,1.55) {(unused)};
  % array segment
  \node[seg, minimum height=12mm, fill=acc!8] (arr) at (0,0.6)
        {array: four \texttt{.quad}s};
  \node[anchor=east, font=\scriptsize] at (-2.0,1.2) {$\mathtt{0x088}$};
  \node[anchor=east, font=\scriptsize] at (-2.0,0.0) {$\mathtt{0x068}$};
  % code segment
  \node[seg, minimum height=18mm] (code) at (0,-0.9)
        {code: driver, then \texttt{sum}};
  \node[anchor=east, font=\scriptsize] at (-2.0,-1.8) {$\mathtt{0x000}$};
\end{tikzpicture}
$$

Three patterns are worth naming. The loop is entered at its **test** (`jmp test`),
not the body — the standard guard for a count that might be zero: the body runs only
once the test confirms work remains. The test is `andq %rsi, %rsi`, the Y86-64 idiom
for "set condition codes from a register without changing it" (since $x \mathbin{\&}
x = x$); on the path arriving via `jmp test`, no arithmetic has touched the count, so
the codes must be established before `jne` reads them. Inside the loop the pointer
walks by 8 with `addq %r8, %rdi` rather than recomputing `base + 8*i` each pass — the
strength-reduction answer to Y86-64's
[missing scaled-index mode](/computer-architecture/instruction-set-architecture/addressing-modes),
with the stride and decrement parked in `%r8` and `%r9` because `addq` and `subq`
take only register operands.

To see the loop actually compute, trace the registers across the four iterations on
the array `[10, 20, 30, 40]`. Entry has `%rdi` pointing at `array` (call it `A`),
`%rsi = 4`, and `%rax = 0`. Each pass loads `*%rdi`, adds it to the total, advances
the pointer by 8, and decrements the count; the loop exits when the count hits zero.

$$
% caption: Register state across the four loop iterations of sum on [10,20,30,40].
% caption: Each pass adds the current element to rax, advances rdi by 8 (the pointer
% caption: walks A, A+8, A+16, A+24), and decrements rsi; when rsi reaches 0 the jne
% caption: falls through and rax holds the total 100.
\begin{tikzpicture}[font=\footnotesize,
  h/.style={anchor=west, text=acc, font=\footnotesize\ttfamily},
  c/.style={anchor=west, font=\ttfamily\footnotesize}]
  \definecolor{acc}{HTML}{2348F2}
  \draw[acc!40] (-0.3,0.45) -- (10.6,0.45);
  \node[h] at (-0.2,0.8) {pass};
  \node[h] at (1.5,0.8)  {loads \texttt{*rdi}};
  \node[h] at (4.4,0.8)  {rax after};
  \node[h] at (6.9,0.8)  {rdi after};
  \node[h] at (9.2,0.8)  {rsi after};
  \foreach \y/\p/\ld/\ra/\rd/\rs in {
    0/1/{10}/{10}/{A+8}/{3},
    -0.6/2/{20}/{30}/{A+16}/{2},
    -1.2/3/{30}/{60}/{A+24}/{1},
    -1.8/4/{40}/{100}/{A+32}/{0}} {
    \node[c] at (-0.2,\y) {\p};
    \node[c] at (1.5,\y)  {\ld};
    \node[c] at (4.4,\y)  {\ra};
    \node[c] at (6.9,\y)  {\rd};
    \node[c] at (9.2,\y)  {\rs};
  }
  \node[c, text=acc] at (-0.2,-2.55) {exit: rsi = 0, \texttt{jne} falls through, rax = 100};
\end{tikzpicture}
$$

The pointer `%rdi` walks `A, A+8, A+16, A+24` — one stride per pass, never
recomputed from a base and index — and `%rsi` counts `4, 3, 2, 1, 0`. On the pass
that drives `%rsi` to 0, the `andq` sets `ZF = 1`, `jne` falls through, and `%rax`
holds `10 + 20 + 30 + 40 = 100`. The loop body has no multiply and no scaled
index, just a load and three adds.

$$
% caption: Control flow of sum. The entry jump lands on the test, so a zero count
% caption: never executes the body; while the count stays nonzero, jne loops back
% caption: through the body; when it hits zero, control falls through to ret.
\begin{tikzpicture}[font=\footnotesize, >=stealth,
  blk/.style={draw, minimum width=34mm, minimum height=9mm, align=center,
              font=\ttfamily\footnotesize}]
  \definecolor{acc}{HTML}{2348F2}
  \node[blk] (init) at (0,2.0) {irmovq, irmovq\\xorq \%rax,\%rax};
  \node[blk, draw=acc, thick, text=acc] (test) at (0,0) {test:\\andq, jne};
  \node[blk, minimum height=14mm] (body) at (5.6,0)
        {loop:\\mrmovq, addq\\addq, subq};
  \node[blk, minimum width=16mm] (ret) at (0,-2.0) {ret};
  \draw[->] (init) -- (test) node[midway, right, font=\scriptsize] {jmp test};
  \draw[->, acc] ([yshift=2mm]test.east) -- ([yshift=2mm]body.west)
        node[midway, above, font=\scriptsize] {count != 0};
  \draw[->] ([yshift=-2mm]body.west) -- ([yshift=-2mm]test.east)
        node[midway, below, font=\scriptsize] {back to test};
  \draw[->] (test) -- (ret) node[midway, right, font=\scriptsize] {count $=$ 0};
\end{tikzpicture}
$$

## From assembly to bytes

The assembler walks the listing, tracking the location counter, and emits each
instruction's bytes exactly as the encoding rules dictate. Resolving the labels
gives `sum` = `0x028`, `loop` = `0x047`, `test` = `0x057`, `array` = `0x068`, and
`stack` = `0x200`, and every reference is filled in little-endian. The result is the
program's **memory image**: a map from address to bytes.

$$
% caption: The assembled memory image, address to bytes. Each instruction's first
% caption: byte is icode:ifun; label references resolve to little-endian addresses
% caption: (call sum -> 80 28 00..., array -> 30 f7 68 00...). The image is exactly
% caption: what the processor fetches and executes.
\begin{tikzpicture}[font=\ttfamily\footnotesize,
  a/.style={anchor=east, text=acc, font=\ttfamily\footnotesize},
  b/.style={anchor=west}]
  \definecolor{acc}{HTML}{2348F2}
  \def\rh{0.52}
  \foreach \i/\addr/\bytes/\asm in {
    0/{0x000}/{30 f4 00 02 00 00 00 00 00 00}/{irmovq stack,\%rsp},
    1/{0x00a}/{30 f7 68 00 00 00 00 00 00 00}/{irmovq array,\%rdi},
    2/{0x014}/{30 f6 04 00 00 00 00 00 00 00}/{irmovq {\char36}4,\%rsi},
    3/{0x01e}/{80 28 00 00 00 00 00 00 00}/{call sum},
    4/{0x027}/{00}/{halt},
    5/{0x028}/{30 f8 08 00 00 00 00 00 00 00}/{irmovq {\char36}8,\%r8},
    6/{0x032}/{30 f9 01 00 00 00 00 00 00 00}/{irmovq {\char36}1,\%r9},
    7/{0x03c}/{63 00}/{xorq \%rax,\%rax},
    8/{0x03e}/{70 57 00 00 00 00 00 00 00}/{jmp test},
    9/{0x047}/{50 a7 00 00 00 00 00 00 00 00}/{mrmovq (\%rdi),\%r10},
    10/{0x051}/{60 a0}/{addq \%r10,\%rax},
    11/{0x053}/{60 87}/{addq \%r8,\%rdi},
    12/{0x055}/{61 96}/{subq \%r9,\%rsi},
    13/{0x057}/{62 66}/{andq \%rsi,\%rsi},
    14/{0x059}/{74 47 00 00 00 00 00 00 00}/{jne loop},
    15/{0x062}/{90}/{ret}} {
    \node[a] at (0,-\i*\rh) {\addr:};
    \node[b] at (0.25,-\i*\rh) {\bytes};
    \node[anchor=west, font=\ttfamily\footnotesize, text=black]
      at (6.6,-\i*\rh) {\asm};
  }
\end{tikzpicture}
$$

Read the image against the reference and every byte checks out: `call sum` is `80`
followed by `0x28` little-endian; `irmovq array,%rdi` is `30 f7` (no source, dest
`%rdi` = 7) then `0x68` little-endian; `addq %r10,%rax` is `60` then `a0` (`%r10` =
A, `%rax` = 0); `mrmovq (%rdi),%r10` is `50` then `a7` (dest `%r10` = A, base `%rdi`
= 7) with a zero displacement. The jumps `jmp test` (`70 57...`) and `jne loop`
(`74 47...`) carry their resolved targets directly — absolute 8-byte addresses,
since Y86-64 has no PC-relative branches. The four `.quad`s lay `0a 14 1e 28` —
ten, twenty, thirty, forty — at `0x068` onward, eight bytes apiece.

## The stack during the call

The program's one subroutine call exercises the stack. `call sum`
executes two updates atomically, then `ret` reverses them:

$$
\texttt{call}:\quad \texttt{\%rsp} \gets \texttt{\%rsp} - 8;\ \ M[\texttt{\%rsp}] \gets \text{PC}_{\text{ret}};\ \ \text{PC} \gets \texttt{sum},
$$
$$
\texttt{ret}:\quad \text{PC} \gets M[\texttt{\%rsp}];\ \ \texttt{\%rsp} \gets \texttt{\%rsp} + 8.
$$

The return address $\text{PC}_{\text{ret}}$ is the instruction after the `call`,
here `0x027`. The stack grows _downward_ from `stack` = `0x200`, so the pushed
address lands at $\mathtt{0x200} - 8 = \mathtt{0x1f8}$.

$$
% caption: The stack across call sum and ret. call decrements %rsp by 8 and writes
% caption: the return address 0x027 at the new top (0x1f8); ret reads it back into
% caption: the PC and restores %rsp to 0x200. The stack grows toward lower addresses.
\begin{tikzpicture}[font=\footnotesize, >=stealth,
  cell/.style={draw, minimum width=26mm, minimum height=8mm, align=center}]
  \definecolor{acc}{HTML}{2348F2}
  % before
  \node[font=\footnotesize, text=acc] at (0,1.5) {before \texttt{call}};
  \node[cell] (b0) at (0,0.5) {...};
  \node[cell] (b1) at (0,-0.4) {(unused)};
  \node[anchor=west, font=\scriptsize] at (1.5,0.5) {$\mathtt{0x200}=\mathtt{\%rsp}$};
  % after
  \node[font=\footnotesize, text=acc] at (6.4,1.5) {after \texttt{call}};
  \node[cell] (a0) at (6.4,0.5) {...};
  \node[cell, fill=acc!8, text=acc, thick] (a1) at (6.4,-0.4) {ret addr $\mathtt{0x027}$};
  \node[anchor=west, font=\scriptsize] at (7.9,0.5) {$\mathtt{0x200}$};
  \node[anchor=west, font=\scriptsize, text=acc]
    at (7.9,-0.4) {$\mathtt{0x1f8}=\mathtt{\%rsp}$};
  % grow-down arrow
  \draw[->] (-1.7,1.0) -- (-1.7,-0.9)
    node[midway, left, font=\scriptsize, align=center] {grows\\down};
\end{tikzpicture}
$$

This hand-built stack is the same machinery the
[procedures](/computer-architecture/machine-level-x86-64/procedures) lesson treats
in x86-64, stripped to its essentials: `call` and `ret` are nothing more than a
push and a pop of the program counter.

## A second pattern: branch-free max

The array sum exercises loops; the second pattern is the **branch-free
conditional**, the reason `cmovXX` is in the instruction set. Here is `long max(long a, long b)` with `a` in `%rdi` and `b` in `%rsi`,
returning in `%rax`:

```asm [max.ys]
# long max(long a, long b)
max:
rrmovq %rdi, %rax       # result = a, provisionally
rrmovq %rsi, %r10       # copy b: subq would destroy it
subq %rdi, %r10         # %r10 = b - a, sets CC
cmovg %rsi, %rax        # if b > a, result = b
ret
```

Two Y86-64 habits show up in four instructions. First, the comparison costs a
spare register: Y86-64 has no `cmpq`, so testing $b - a$ means running `subq`, and
`subq %rdi, %r10` overwrites `%r10` — hence the copy, keeping `%rsi` intact for the
`cmovg`. Second, the conditional is straight-line: every execution runs all
instructions, and the condition governs only whether the move commits,

$$
R[\texttt{rax}] \gets \begin{cases} R[\texttt{rsi}] & b - a > 0 \\ R[\texttt{rax}] & \text{otherwise.} \end{cases}
$$

The branching version (`jle` around an `rrmovq`) computes the same answer, but its
jump is unpredictable when the data is, and the
[pipelined processor](/computer-architecture/pipelining/control-hazards-and-branch-prediction)
pays for every misprediction. `cmovg` reads the codes `subq` set — `g` holds exactly
when $b - a > 0$ with the
[overflow correction](/computer-architecture/instruction-set-architecture/the-y86-64-instruction-set)
folded in — and the PC never leaves the straight path.

With complete programs assembled into bytes, a working stack, and both loop and
conditional idioms in hand, every piece the next module needs is in place: the
[sequential processor](/computer-architecture/processor-design/the-fetch-decode-execute-cycle)
fetches and decodes byte images in exactly this format, programs just like this
one, one instruction at a time.

## The convention in the real world

The calling convention this lesson borrows wholesale from x86-64 is the **System V
AMD64 ABI**, the same document every C compiler, linker, and debugger on Linux and
macOS agrees to. A few of its real-world corners are worth knowing, because they are
exactly the parts Y86-64's tiny programs never grow large enough to need.

**Stack frames and the frame pointer.** The `sum` procedure above uses no stack
space of its own — it keeps everything in registers, so its "frame" is just the
return address `call` pushed. Real procedures with locals or many live values
allocate a **stack frame**: they subtract from `%rsp` on entry to reserve space, keep
locals at fixed displacements from the frame base, and add it back on
exit.[^sysv] CS:APP's [procedures lesson](/computer-architecture/machine-level-x86-64/procedures)
develops this in x86-64; the Y86-64 mechanics are identical, because `call`, `ret`,
`pushq`, and `popq` are the same, only the programs here stay small enough to dodge
the frame.

**The red zone.** The System V ABI reserves 128 bytes _below_ `%rsp` — the **red
zone** — that a leaf function (one that calls nothing) may scribble in without
adjusting `%rsp` at all, because no `call` will overwrite it. This is a pure
performance convention: it saves the `sub`/`add` pair around `%rsp` in the common
case of a small helper, exactly the kind of function `sum` is. Nothing in the
hardware knows about the red zone; it is a promise the compiler and the ABI make to
each other, in precisely the sense the [first lesson](/computer-architecture/instruction-set-architecture/what-an-isa-is)
gave the word "convention."

**Why `andq x, x` and not a compare.** The loop-entry idiom of this lesson —
`andq %rsi, %rsi` to set the flags from a register without changing it — is a
Y86-64 necessity, but the same shape appears in real x86-64: `testq %rsi, %rsi` is
the standard compiler emission for "is this register zero or negative?", chosen over
`cmpq $0, %rsi` because it is one byte shorter and needs no immediate. The idiom this
module frames as a workaround for Y86-64's missing compare turns out to be what
production compilers emit anyway, for the density reason.

> **Takeaway.** A Y86-64 program places code and data with `.pos`, `.align`, and
> `.quad`, borrows x86-64's calling convention (arguments in `%rdi`, `%rsi`, ...;
> result in `%rax`), and sets up its own stack by loading `%rsp` from a reserved
> region. The assembler turns the listing into an exact memory image — `call sum`
> becomes `80 28 00 …`, the array becomes a run of `.quad`s — and `call`/`ret`
> push and pop the return address as the stack grows downward. The two idioms to
> keep: enter loops at the test with the `andq x, x` flag-setting trick (the same
> `testq` real compilers emit), and replace short branches with `cmovXX`
> straight-line code.

[^sysv]: **M. Matz, J. Hubička, A. Jaeger, M. Mitchell, eds.**, _System V Application
    Binary Interface, AMD64 Architecture Processor Supplement_ — the System V AMD64
    ABI, which fixes argument registers (`%rdi`, `%rsi`, `%rdx`, `%rcx`, `%r8`, `%r9`),
    the return register (`%rax`), caller/callee-saved partitioning, the 128-byte red
    zone (§3.2.2), and 16-byte stack alignment at call boundaries. Y86-64 adopts a
    subset of this convention; CS:APP §3.7 develops the x86-64 stack discipline.
