Output & Running It
Hello, World
A Pascal program has a name, a
begin and an end. with a full stop. An assembly program has none of those — the linker looks for a symbol called _start and that is where the process begins. db declares bytes; 10 is a newline. equ $ - message computes the length at assemble time, since $ means "the address here".program HelloWorld;
begin
WriteLn('Hello, World!');
end.global _start
section .data
message: db "Hello, World!", 10
length: equ $ - message
section .text
_start:
mov rax, 1 ; syscall 1 = write
mov rdi, 1 ; fd 1 = stdout
mov rsi, message ; the address of the bytes
mov rdx, length ; how many bytes to write
syscall
mov rax, 60 ; syscall 60 = exit
xor rdi, rdi ; status 0
syscallWriteLn is the whole difference: it knows the argument is a string, finds its length, writes it, appends the newline, and flushes through a buffered text file variable. The assembly version spells the newline as the byte 10 and the length as a number computed by the assembler. Note also that Pascal's end. returns to a runtime that then exits; here nothing runs after _start unless you write it.Halt Sets the Exit Status
Halt(N) stops the program immediately with status N, skipping any remaining statements. In assembly the status is simply whatever is in rdi when the exit syscall runs.program HaltWithStatus;
begin
WriteLn('about to halt with 3');
Halt(3);
end.global _start
section .data
message: db "about to halt with 3", 10
length: equ $ - message
section .text
_start:
mov rax, 1
mov rdi, 1
mov rsi, message
mov rdx, length
syscall
mov rax, 60 ; exit
mov rdi, 3 ; ...with status 3
syscallThere is no way to fall off the end of an assembly program safely: without that final syscall the processor keeps executing whatever bytes follow in
.text and the process dies on an illegal instruction. Pascal's end. is a return into runtime code that calls exit for you, which is why forgetting Halt is harmless and forgetting the syscall is not.What WriteLn(number) Hides
This is the only row that spells number-to-text conversion out in full; later rows print one or two digits so the conversion stays three lines.
div rcx divides the 128-bit value rdx:rax by rcx, leaving the quotient in rax and the remainder in rdx — so rdx must be zeroed first. Digits emerge least-significant first, which is why the buffer fills backwards.program WriteNumber;
var
value: LongInt;
begin
value := 31415;
WriteLn(value);
end.global _start
section .bss
buffer: resb 20
section .text
_start:
mov rax, 31415
lea rsi, [buffer + 20] ; one past the end — we fill backwards
mov rcx, 10
dec rsi ; make room for the newline first
mov byte [rsi], 10
convert:
xor rdx, rdx ; clear the high half of the dividend
div rcx ; rax = rax / 10, rdx = rax % 10
add dl, '0'
dec rsi
mov [rsi], dl
test rax, rax
jnz convert
lea rdx, [buffer + 20]
sub rdx, rsi ; length = end - start
mov rax, 1
mov rdi, 1
syscall
mov rax, 60
xor rdi, rdi
syscallSixteen instructions and one division per digit.
WriteLn(value) dispatches on the argument's type at compile time, picks the integer-writing routine out of the runtime, and that routine performs this same loop — written once by the FPC authors and shared by every Pascal program. Notice also that nothing here reports failure: a buffer too small would simply write past it.Pascal Can Write Assembly Itself
asm ... end; Is a Pascal Statement
This is the row that makes the pair special: Free Pascal has a built-in assembler, so the two columns of this page can meet inside one file.
{$asmmode intel} selects the same Intel syntax NASM uses. The block reads and writes ordinary Pascal variables by name, because the compiler knows where it put them. The Pascal column is illustrative rather than runnable here for a reason worth stating: an x86-64 asm block is not portable code, and it will not compile on an ARM machine at all.program InlineAssembler;
{$asmmode intel}
var
first, second, total: LongInt;
begin
first := 40;
second := 2;
asm
mov eax, first
add eax, second
mov total, eax
end;
WriteLn(total);
end.global _start
section .bss
output: resb 3
section .text
_start:
mov eax, 40 ; the same three instructions,
add eax, 2 ; with the names supplied by hand
; total is now in eax
xor rdx, rdx
mov rcx, 10
div rcx
add al, '0'
add dl, '0'
mov [output], al
mov [output + 1], dl
mov byte [output + 2], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 3
syscall
mov rax, 60
xor rdi, rdi
syscallThe instructions are identical; only the operand names differ. Writing
mov eax, first works because the compiler substitutes the variable's actual location — a register or a stack slot — which is exactly the bookkeeping the right-hand column does in your head. And the reason this column cannot run on an Apple Silicon machine is the sharpest possible statement of what an asm block costs: it is the one Pascal construct that is not portable at all, because it names a specific processor's instructions. The rest of the program would have compiled anywhere.Variables Are Registers
A var Declaration Becomes a Register
Pascal declares every variable up front in a
var block, with a type. Assembly has sixteen general-purpose registers with fixed names and no types at all — remembering which register holds which of your declarations is your job.program Locals;
var
first, second, total: LongInt;
begin
first := 10;
second := 32;
total := first + second;
WriteLn(total);
end.global _start
section .bss
output: resb 3
section .text
_start:
mov rax, 10 ; first := 10
mov rbx, 32 ; second := 32
add rax, rbx ; total := first + second
xor rdx, rdx
mov rcx, 10
div rcx ; rax = 4, rdx = 2
add al, '0'
add dl, '0'
mov [output], al
mov [output + 1], dl
mov byte [output + 2], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 3
syscall
mov rax, 60
xor rdi, rdi
syscallPascal's insistence on a declaration block ahead of the code is often called ceremony, and this is the argument for it: the compiler needs to know every variable's type and size before it can assign storage, and having them in one place is how it does the job the right-hand column does by hand. Run out of registers and you must choose, yourself, which value gets pushed to memory.
One Register, Four Widths
Pascal names its integer sizes explicitly —
Byte, Word, LongInt, Int64 — and the names are a promise about bit width. Assembly has one register with four names: rax is all 64 bits, eax the low 32, ax the low 16, al the low 8. They are four windows onto the same storage, not four registers.program Widths;
var
value: LongInt;
lowByte: Byte;
begin
value := 7;
value := value + 1;
lowByte := Byte(value);
WriteLn(lowByte);
end.global _start
section .bss
output: resb 2
section .text
_start:
mov rax, 7
add rax, 1 ; rax = 8
; Byte(value) is not a conversion and costs nothing.
; AL *is* the low byte of RAX — the same storage, read narrower.
add al, '0'
mov [output], al
mov byte [output + 1], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 2
syscall
mov rax, 60
xor rdi, rdi
syscallA truncating cast is free for the same reason in both columns: the bits were already there and the narrower name simply stops looking at the rest. The direction that costs an instruction is widening a signed value, where the sign bit has to be smeared across the new high bits — that is
movsx, and it exists because reading al into rax would otherwise leave the top 56 bits stale.Arithmetic & The Flags Register
div and mod Are One Instruction
Pascal spells integer division
div and remainder mod as separate operators. One div instruction produces both at once — quotient in rax, remainder in rdx — and rdx must be zeroed first because it supplies the high half of the dividend.program DivAndMod;
var
quotient, remainder: LongInt;
begin
quotient := 17 div 5;
remainder := 17 mod 5;
WriteLn(quotient);
WriteLn(remainder);
end.global _start
section .bss
output: resb 4
section .text
_start:
mov rax, 17
xor rdx, rdx
mov rcx, 5
div rcx ; ONE instruction: rax = 3, rdx = 2
add al, '0'
mov [output], al
mov byte [output + 1], 10
add dl, '0'
mov [output + 2], dl
mov byte [output + 3], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 4
syscall
mov rax, 60
xor rdi, rdi
syscallWriting
17 div 5 and 17 mod 5 as separate expressions asks for the same division twice unless the compiler notices they are identical — which FPC does. The instruction was always going to produce both halves; Pascal simply has no syntax for asking for them together, so the optimizer has to infer it.The Flags Nobody Declares
Every arithmetic instruction quietly updates a flags register that nothing in the source mentions.
sub sets the carry flag when the subtraction borrowed, and jc jumps if it did — the pair is a range check written at machine level.program Underflow;
var
smaller, larger: LongInt;
begin
smaller := 3;
larger := 10;
if smaller < larger then
WriteLn('underflow')
else
WriteLn(smaller - larger);
end.global _start
section .data
underflow_message: db "underflow", 10
underflow_length: equ $ - underflow_message
section .text
_start:
mov rax, 3
sub rax, 10 ; borrows, so CF = 1 — and nothing declared CF
jc report_underflow
; the else branch would print rax here
mov rax, 60
xor rdi, rdi
syscall
report_underflow:
mov rax, 1
mov rdi, 1
mov rsi, underflow_message
mov rdx, underflow_length
syscall
mov rax, 60
xor rdi, rdi
syscallPascal's
if smaller < larger compiles to a compare and a jump reading exactly this flag. The flag itself is invisible, unnamed, and overwritten by the very next arithmetic instruction, which is why the jump must come immediately after the sub — inserting an unrelated add between them silently breaks the test with nothing to warn you.Control Flow: cmp and jump
if / then / else
cmp is a subtraction that discards the result and keeps only the flags; the conditional jump after it reads them. The two are one thought split across two instructions, and the jump is named for the comparison you meant.program IfThenElse;
var
value: LongInt;
begin
value := 7;
if value > 5 then
WriteLn('big')
else
WriteLn('small');
end.global _start
section .data
big_message: db "big", 10
big_length: equ $ - big_message
small_message: db "small", 10
small_length: equ $ - small_message
section .text
_start:
mov rax, 7
cmp rax, 5
jle print_small ; jump if NOT greater — the condition is inverted
mov rsi, big_message
mov rdx, big_length
jmp print
print_small:
mov rsi, small_message
mov rdx, small_length
print:
mov rax, 1
mov rdi, 1
syscall
mov rax, 60
xor rdi, rdi
syscallThe condition is inverted, which is the commonest confusion when reading compiler output: the source says "if this is true, do the block" and the machine says "if this is false, skip the block". Note too that the two arms had to be arranged so control rejoins at
print — Pascal's if/else has one exit, and here you build that out of a jump.A for Loop
Pascal's
for has a fixed bound evaluated once and a control variable the standard says you must not modify inside the body. Down here there is a register, an increment, and a backwards jump. rbx holds the counter because syscall destroys rcx, and the loop writes on every pass.program CountedLoop;
var
index: LongInt;
begin
for index := 0 to 4 do
WriteLn(index);
end.global _start
section .bss
digit: resb 2
section .text
_start:
xor rbx, rbx ; index := 0
next:
mov rax, rbx
add al, '0'
mov [digit], al
mov byte [digit + 1], 10
mov rax, 1
mov rdi, 1
mov rsi, digit
mov rdx, 2
syscall ; destroys rcx and r11 — rbx survives
inc rbx
cmp rbx, 5
jl next
mov rax, 60
xor rdi, rdi
syscallThe rule that you may not assign to a Pascal
for variable inside the loop exists so the compiler is free to keep it in a register and to strength-reduce the loop — the guarantee is what buys the optimization. The choice of rbx over rcx here is the same kind of decision, made by hand: a counter in rcx would be destroyed by the syscall in the middle of the body.Procedures, The Stack & The Convention
Passing Arguments
The System V ABI names six registers for integer arguments in order —
rdi, rsi, rdx, rcx, r8, r9 — and returns in rax. Pascal's function declares the same contract in a form the compiler checks.program Arguments;
function Combine(first, second, third: LongInt): LongInt;
begin
Combine := first + second * third;
end;
begin
WriteLn(Combine(2, 5, 8));
end.global _start
section .bss
output: resb 3
section .text
; Combine(first in rdi, second in rsi, third in rdx) -> rax
combine:
mov rax, rsi
imul rax, rdx
add rax, rdi
ret
_start:
mov rdi, 2
mov rsi, 5
mov rdx, 8
call combine ; 2 + 5 * 8 = 42
xor rdx, rdx
mov rcx, 10
div rcx
add al, '0'
add dl, '0'
mov [output], al
mov [output + 1], dl
mov byte [output + 2], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 3
syscall
mov rax, 60
xor rdi, rdi
syscallA parameter list is a promise about registers that nothing enforces down here — calling
combine having set only rdi is not an error, and the function reads whatever rsi and rdx happened to hold. Note also the old Pascal convention on display: assigning to the function's own name is how you set the result, which is why Combine := … is an assignment rather than a return.A Stack Frame By Hand
When a procedure has more live values than registers, the extras live on the stack. The three-instruction opening — push the old frame pointer, point
rbp at the current top, lower rsp to reserve space — is a stack frame, and the slots are addressed as negative offsets from rbp. The stack grows downward, which is why reserving subtracts.program StackFrame;
function SumOfThree: LongInt;
var
first, second, third: LongInt;
begin
first := 20;
second := 14;
third := 8;
SumOfThree := first + second + third;
end;
begin
WriteLn(SumOfThree);
end.global _start
section .bss
output: resb 3
section .text
sum_of_three:
push rbp ; save the caller's frame pointer
mov rbp, rsp ; this frame starts here
sub rsp, 24 ; room for three 8-byte locals
mov qword [rbp - 8], 20
mov qword [rbp - 16], 14
mov qword [rbp - 24], 8
mov rax, [rbp - 8]
add rax, [rbp - 16]
add rax, [rbp - 24]
mov rsp, rbp ; discard the locals
pop rbp
ret
_start:
call sum_of_three
xor rdx, rdx
mov rcx, 10
div rcx
add al, '0'
add dl, '0'
mov [output], al
mov [output + 1], dl
mov byte [output + 2], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 3
syscall
mov rax, 60
xor rdi, rdi
syscallPascal's
var block inside a procedure is precisely this sub rsp, 24: the compiler adds up the declared sizes and reserves that many bytes in one instruction. Note that mov rsp, rbp erases nothing — it moves a number, and the locals sit in memory until the next call overwrites them, which is why a pointer to a dead local gives plausible garbage rather than an immediate crash.A var Parameter Is a Pointer
A var Parameter Is an Address
This is the row that makes "pass by reference" concrete. A
var parameter arrives as an address in a register, and every use of the name inside the procedure becomes a memory access through it. Square brackets mean "the contents of", so add [rdi], rsi adds into the eight bytes living at the address in rdi.program VarParameter;
procedure AddTo(var target: LongInt; amount: LongInt);
begin
target := target + amount;
end;
var
total: LongInt;
begin
total := 40;
AddTo(total, 2);
WriteLn(total);
end.global _start
section .data
total: dq 40 ; an 8-byte value in memory
section .bss
output: resb 3
section .text
; AddTo(target ADDRESS in rdi, amount in rsi)
add_to:
add [rdi], rsi ; target := target + amount
ret
_start:
lea rdi, [total] ; passing var total means passing its ADDRESS
mov rsi, 2
call add_to
mov rax, [total] ; read it back: 42
xor rdx, rdx
mov rcx, 10
div rcx
add al, '0'
add dl, '0'
mov [output], al
mov [output + 1], dl
mov byte [output + 2], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 3
syscall
mov rax, 60
xor rdi, rdi
syscallRemoving the word
var from the Pascal signature changes lea rdi, [total] into mov rdi, [total] — the address becomes the value — and the caller's variable stops changing. That one-instruction difference is the entire semantics of var, and it is also why a var parameter cannot accept a literal: there is no address to take.Nested Procedures Need a Static Link
A Nested Procedure Needs a Static Link
Pascal lets a procedure be declared inside another and reach the outer one's variables by name. That is not free: the inner procedure must be told where the outer procedure's frame is, and the address is passed as a hidden extra argument — the static link. C has no equivalent mechanism at all.
program NestedProcedure;
procedure Outer;
var
total: LongInt;
procedure Inner(amount: LongInt);
begin
{ 'total' belongs to Outer's frame, not Inner's. }
total := total + amount;
end;
begin
total := 40;
Inner(2);
WriteLn(total);
end;
begin
Outer;
end.global _start
section .bss
output: resb 3
section .text
; Inner(static link to Outer's frame in rdi, amount in rsi).
; The link is the HIDDEN argument Pascal passes for you.
inner:
add [rdi], rsi ; total := total + amount, through the link
ret
outer:
push rbp
mov rbp, rsp
sub rsp, 16 ; Outer's frame; total lives at [rbp - 8]
mov qword [rbp - 8], 40
lea rdi, [rbp - 8] ; the static link: where Outer's total lives
mov rsi, 2
call inner
mov rax, [rbp - 8]
mov rsp, rbp
pop rbp
ret
_start:
call outer
xor rdx, rdx
mov rcx, 10
div rcx
add al, '0'
add dl, '0'
mov [output], al
mov [output + 1], dl
mov byte [output + 2], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 3
syscall
mov rax, 60
xor rdi, rdi
syscallThe hidden argument is why a nested procedure cannot be called from outside its parent — there would be no frame to link to — and why taking a procedure variable pointing at a nested procedure is restricted. Nesting is genuinely useful and genuinely not free: every call to
Inner carries one more register than its written parameter list suggests.A ShortString Knows Its Own Length
The Length Lives at Offset 0
A classic Pascal
ShortString stores its length in the byte at offset 0, with the characters following. So Length(s) is one memory read — never a scan — and the maximum length is 255 because that is what one byte holds. movzx loads a narrow value into a wide register and zeroes the rest.program ShortStringLength;
var
message: ShortString;
begin
message := 'Hello';
WriteLn(Length(message));
WriteLn(message);
end.global _start
section .data
; A ShortString laid out by hand: length byte first, then the characters.
message: db 5, "Hello"
section .bss
output: resb 2
section .text
_start:
movzx rax, byte [message] ; Length(message) — ONE read, no scan
add al, '0'
mov [output], al
mov byte [output + 1], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 2
syscall
mov rax, 1
mov rdi, 1
lea rsi, [message + 1] ; the characters start AFTER the length byte
movzx rdx, byte [message]
syscall
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
mov rax, 60
xor rdi, rdi
syscall
section .data
newline: db 10This layout is Pascal's oldest and best-known design decision, and it is the direct opposite of C's: a stored count rather than a terminator. The cost is the 255-byte ceiling and a fixed 256-byte footprint; the benefits are that length is free, a string may contain any byte including zero, and no operation can run off the end looking for something that is not there. Modern
AnsiString keeps the stored length and moves it to a header before the characters, which is why the character pointer still looks like a C string.Arrays & Records Are Offsets
Indexing, and Where the Lower Bound Went
Pascal arrays declare their own index range, so
array[1..3] starts at 1. The machine only knows offsets from a base address, so the compiler subtracts the lower bound — an index of 1 becomes an offset of 0. [rsi + rcx * 8] multiplies the index by 8 as part of the instruction.program ArrayIndex;
var
numbers: array[1..3] of LongInt;
begin
numbers[1] := 10;
numbers[2] := 20;
numbers[3] := 30;
WriteLn(numbers[2]);
end.global _start
section .data
numbers: dq 10, 20, 30
section .bss
output: resb 3
section .text
_start:
lea rsi, [numbers]
mov rcx, 2 ; the Pascal index
dec rcx ; MINUS the lower bound — this is the whole trick
mov rax, [rsi + rcx * 8]
xor rdx, rdx
mov rcx, 10
div rcx
add al, '0'
add dl, '0'
mov [output], al
mov [output + 1], dl
mov byte [output + 2], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 3
syscall
mov rax, 60
xor rdi, rdi
syscallThe
dec rcx is free in practice — the compiler folds the subtraction into the addressing mode's displacement, so array[1..3] and array[0..2] generate identical code. That is the answer to the old complaint that 1-based indexing must be slower: the lower bound is a compile-time constant, and constants cost nothing. What it does buy is that the declared range is checkable, which {$rangechecks on} turns into a real test.A Record Is a Set of Offsets
A field name is a compile-time constant added to a base address.
[rbx + 8] reaches the second 8-byte field — the name score exists only in the source, and the 8 is what it becomes.program RecordOffsets;
type
TRecord = record
identifier: Int64;
score: Int64;
end;
var
entry: TRecord;
begin
entry.identifier := 7;
entry.score := 42;
WriteLn(entry.score);
end.global _start
section .data
; record identifier: Int64; score: Int64; end
entry: dq 7 ; + 0 identifier
dq 42 ; + 8 score
section .bss
output: resb 3
section .text
_start:
lea rbx, [entry]
mov rax, [rbx + 8] ; entry.score
xor rdx, rdx
mov rcx, 10
div rcx
add al, '0'
add dl, '0'
mov [output], al
mov [output + 1], dl
mov byte [output + 2], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 3
syscall
mov rax, 60
xor rdi, rdi
syscallField access is free — it is part of the addressing mode, not an extra instruction. What is not free is assuming you know the offsets: Free Pascal inserts padding so each field lands on its natural alignment, and
packed record is the directive that suppresses it. Writing the 8 by hand, as the right-hand column does, is only correct because both fields happen to be eight bytes wide.Pointers Are Just Numbers
The Caret Is a Memory Access
Pascal writes
@value to take an address and pointer^ to follow one. lea computes the address a bracket expression would read from and hands you the number; mov with brackets performs the read. The difference between them is exactly one memory access.program Pointers;
var
value: LongInt;
reference: ^LongInt;
begin
value := 7;
reference := @value;
WriteLn(reference^);
end.global _start
section .data
value: dq 7
section .bss
output: resb 2
section .text
_start:
lea rbx, [value] ; reference := @value — the ADDRESS
mov rax, [rbx] ; reference^ — the CONTENTS
add al, '0'
mov [output], al
mov byte [output + 1], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 2
syscall
mov rax, 60
xor rdi, rdi
syscallPascal puts the dereference after the name, which reads more naturally in a chain —
node^.next^.value goes left to right — while C's prefix * needs parentheses for the same idea. Down here neither notation exists: a bare label is a number and brackets are a memory access, and forgetting the brackets gives you an address where you wanted a value, with no type to object.Bit Manipulation
and, or, xor Are These Instructions
Pascal spells the bitwise operators as words —
and, or, xor, shl, shr — and each is exactly one machine instruction with the same name. This is the one place on the page where the two columns line up word for word.program Bitwise;
var
flags: LongInt;
begin
flags := %1010;
WriteLn(flags and %0010);
WriteLn(flags or %0001);
WriteLn(flags xor %1111);
end.global _start
section .bss
output: resb 7
section .text
_start:
mov rax, 0b1010
and rax, 0b0010 ; 2
add al, '0'
mov [output], al
mov byte [output + 1], 10
mov rax, 0b1010
or rax, 0b0001 ; 11 — two digits, so divide
xor rdx, rdx
mov rcx, 10
div rcx
add al, '0'
add dl, '0'
mov [output + 2], al
mov [output + 3], dl
mov byte [output + 4], 10
mov rax, 0b1010
xor rax, 0b1111 ; 5
add al, '0'
mov [output + 5], al
mov byte [output + 6], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 7
syscall
mov rax, 60
xor rdi, rdi
syscallThe
%1010 notation is Pascal's binary literal and 0b1010 is NASM's — the same bits with different spelling. Note that Pascal uses the same words for boolean and bitwise operations and decides which you meant from the operand types, which is why and on two integers is this instruction while and on two booleans is a branch.Gotchas For Pascal Developers
Nothing Checks the Range
Free Pascal can insert a bounds test on every array access with
{$rangechecks on}, and it reports a runtime error naming the offending index. Assembly has no such option — the check is a comparison you write, or it does not happen.program RangeCheck;
var
numbers: array[1..3] of LongInt;
index: LongInt;
begin
numbers[1] := 10;
numbers[2] := 20;
numbers[3] := 30;
index := 5;
if (index < 1) or (index > 3) then
WriteLn('out of range')
else
WriteLn(numbers[index]);
end.global _start
section .data
numbers: dq 10, 20, 30
out_of_range_message: db "out of range", 10
out_of_range_length: equ $ - out_of_range_message
section .text
_start:
mov rbx, 5
dec rbx ; index minus the lower bound
cmp rbx, 3
jae out_of_range ; unsigned: catches too-large AND "negative"
; the in-range path would read [numbers + rbx * 8] here
mov rax, 60
xor rdi, rdi
syscall
out_of_range:
mov rax, 1
mov rdi, 1
mov rsi, out_of_range_message
mov rdx, out_of_range_length
syscall
mov rax, 60
xor rdi, rdi
syscallOne instruction,
jae, does the work of both halves of Pascal's (index < 1) or (index > 3): after subtracting the lower bound, an index below the range wraps to an enormous unsigned value and fails the same upper test. That trick is exactly what {$rangechecks on} emits, which is why range checking costs two instructions rather than four.A Syscall Destroys Registers
The
syscall instruction always destroys rcx and r11 — the processor uses them to remember how to return. Callee-saved registers (rbx, rbp, r12–r15) survive. This matters directly to anyone writing an asm block: the surrounding compiler-generated code assumes the convention holds.program CalleeSaved;
var
counter: LongInt;
begin
counter := 42;
WriteLn('writing');
{ counter survives the call because the compiler knows the convention }
WriteLn(counter);
end.global _start
section .data
message: db "writing", 10
length: equ $ - message
section .bss
output: resb 3
section .text
_start:
mov rbx, 42 ; rbx is callee-saved — it will survive
mov rcx, 42 ; rcx will NOT
mov rax, 1
mov rdi, 1
mov rsi, message
mov rdx, length
syscall ; rcx is now garbage; rbx is untouched
mov rax, rbx ; read the one that survived
xor rdx, rdx
mov rcx, 10
div rcx
add al, '0'
add dl, '0'
mov [output], al
mov [output + 1], dl
mov byte [output + 2], 10
mov rax, 1
mov rdi, 1
mov rsi, output
mov rdx, 3
syscall
mov rax, 60
xor rdi, rdi
syscallReading
rcx after the syscall would print whatever the kernel left behind — no crash, no complaint, just a wrong answer that changes with the kernel version. This is the concrete hazard behind the earlier asm ... end; row: an inline block that clobbers a callee-saved register without restoring it corrupts the function around it, and the compiler will not notice.