Skip to content

Instantly share code, notes, and snippets.

@tbodt
Created February 3, 2017 23:13
Show Gist options
  • Save tbodt/1898fecdb64d929c23ac1c321ffadf54 to your computer and use it in GitHub Desktop.
Save tbodt/1898fecdb64d929c23ac1c321ffadf54 to your computer and use it in GitHub Desktop.
# Ooohhh! A vending machine!
# This is a time vending machine. You can buy timepieces. You pay by waiting.
#################################### Data segment ####################################
# All sorts of data.
.data
welcome_message:
.int 30
.ascii "Time vending/killing machine\n\n"
seconds_prompt:
.int 9
.ascii "Seconds: "
minutes_prompt:
.int 9
.ascii "Minutes: "
hours_prompt:
.int 7
.ascii "Hours: "
volunteered_1:
.int 21
.ascii "You have volunteered "
volunteered_2:
.int 33
.ascii " seconds of your precious time.\n\n"
dot_space:
.int 2
.ascii ". "
space_lparen:
.int 2
.ascii " ("
space_seconds_rparen_newline:
.int 10
.ascii " seconds)\n"
oscillator_description:
.int 13
.ascii "An oscillator"
metronome_description:
.int 11
.ascii "A metronome"
sundial_description:
.int 9
.ascii "A sundial"
clock_description:
.int 7
.ascii "A clock"
watch_description:
.int 7
.ascii "A watch"
digital_watch_description:
.int 15
.ascii "A digital watch"
# Format: Description address, then price in seconds.
items:
.long oscillator_description
.int 1
.long metronome_description
.int 2
.long sundial_description
.int 10
.long clock_description
.int 30
.long watch_description
.int 60
.long digital_watch_description
.int 3600
items_count:
.int 6
what_to_buy_prompt:
.int 25
.ascii "What do you want to buy? "
thank_you:
.int 60
.ascii "\nThank you for buying something from this vending machine.\n\n"
your_change_1:
.int 15
.ascii "Your change is "
your_change_2:
.int 18
.ascii " seconds. That's:\n"
second_s:
.int 11
.ascii " second(s)\n"
minute_s:
.int 11
.ascii " minute(s)\n"
hour_s:
.int 9
.ascii " hour(s)\n"
#################################### Macros ####################################
# Simplify calling the subroutines. They have to be up here so they can be defined before they are used.
.macro write message
push \message
call write
add $4, %esp
.endm
.macro write_int number
push \number
call write_int
add $4, %esp
.endm
.macro read_int_prompt prompt
push \prompt
call read_int_prompt
add $4, %esp
.endm
.text
.global _start
################################### Actual code ####################################
_start:
write $welcome_message
# %ebx is how many seconds is in the machine
read_int_prompt $seconds_prompt
mov %eax, %ebx
read_int_prompt $minutes_prompt
mov $0, %edx # prepare to multpily
mov $60, %ecx # multiply by 60
mul %ecx
add %eax, %ebx # seconds += minutes * 60
read_int_prompt $hours_prompt
mov $0, %edx # like above
mov $3600, %ecx
mul %ecx
add %eax, %ebx # seconds += hours * 3,600
write $volunteered_1
write_int %ebx
write $volunteered_2
mov $1, %ecx # %ecx is counter, going from 1 to items_count
mov $items, %edx # %edx is current item adress, with 8 (size of item structure) added each time
items_loop:
write_int %ecx
write $dot_space
write 0(%edx) # name address is first field
write $space_lparen
write_int 4(%edx) # price is second field
write $space_seconds_rparen_newline
inc %ecx
add $8, %edx
cmp items_count, %ecx
jle items_loop # if %ecx <= items_count, continue loop
read_int_prompt $what_to_buy_prompt
dec %eax
mov $items, %ecx # only for the purposes of the next line
mov 4(%ecx, %eax, 8), %eax # find out the price, using complicated addressing
# price is in %eax
sub %eax, %ebx # charge them for their purchase
call sleep_for_eax_seconds
write $thank_you
write $your_change_1
write_int %ebx
write $your_change_2
mov %ebx, %eax # can only divide by %eax
mov $60, %ebx # will divide by 60 several times
mov $0, %edx
div %ebx # told you so...
write_int %edx
write $second_s
mov $0, %edx
div %ebx # really, just dividing by 60, printing the remainder, and repeating with the quotient
write_int %edx
write $minute_s
mov $0, %edx
div %ebx # just like in write_int, except with 60 and not 10
write_int %edx
write $hour_s # we're actually printing the number in Babylonian-friendly base 60
# exit
mov $1, %eax # exit syscall number
mov $0, %ebx # exit status
int $0x80
################################### Subroutines ###################################
# Print something, quick and easy.
# Push the address of the output buffer.
write:
# Stack frames are cool!
push %ebp
mov %esp, %ebp
# no need for local variables
pushal # pusha only pushes first 16 bits of register
mov 8(%ebp), %edi # put the address in a register
mov $4, %eax # write syscall number
mov $1, %ebx # stdout
lea 4(%edi), %ecx # address
mov (%edi), %edx # count
int $0x80
# clean up the stack frame
popal # reverse pushal
mov %ebp, %esp
pop %ebp
ret
# I copied these from the ascii-int.asm file, so I could change things I didn't like.
# Write an integer to standard output. Push it on the stack first.
write_int:
# This is a stack frame.
push %ebp
mov %esp, %ebp
push $0 # -4(%ebp): output buffer
pushal # push all registers
mov $0, %edi # %edi is a counter
mov 8(%ebp), %eax # put number to write in %eax
# Divide by 10. Convert the remainder to a character and push it, then repeat with the quotient.
write_int_loop:
mov $0, %edx # prepare to divide
mov $10, %ebx # must divide by a register
div %ebx # ready, set, divide!
add $48, %edx # convert integer to digit
push %edx # a stack is needed to put the characters in the right order
inc %edi # increment counter
cmp $0, %eax # stop when all the digits have been eaten
jne write_int_loop
# Pop the right number of characters from the stack and output them.
write_int_output:
cmp $0, %edi #Loop until we popped everything we pushed
je write_int_done
pop %edx
movb %dl, -4(%ebp) #Put the popped character into our temp variable
mov $4, %eax #Linux/UNIX command for Write
mov $1, %ebx #Linux/UNIX to output to the screen
lea -4(%ebp), %ecx #Linux/UNIX needs an address. This will (L)oad the (E)ffective (A)ddress of the local variable and put it in ECX
mov $1, %edx #Output just one character
int $0x80
sub $1, %edi
jmp write_int_output #Next loop
write_int_done:
popal # pop all registers
# clean up stack frame
mov %ebp, %esp
pop %ebp
ret
# Read an integer from the keyboard. Returns it in %eax. That's it.
read_int:
# This is a stack frame.
push %ebp
mov %esp, %ebp
push $0 # -4(%ebp): input buffer
push $0 # -8(%ebp): running total
push $0 # -12(%ebp): whether to ignore rest of line, using C booleans
push %ebx
push %ecx
push %edx
push %edi
read_int_loop:
mov $3, %eax # read syscall
mov $0, %ebx # stdin
lea -4(%ebp), %ecx # address (look, it's the address of the input buffer)
mov $1, %edx # read just one character
int $0x80
mov $0, %ecx # zero the top 24 bits
movb -4(%ebp), %cl # put the character in the bottom 8
cmp $10, %cl # if the character is a newline, we're done
je read_int_done
cmp $1, -12(%ebp) # if ignoring the rest of the line, ignore this character, and the others will be ignored
je read_int_loop
cmp $48, %cl # If the character is less than '0',
jl read_int_invalid
cmp $57, %cl # or the character is greater than '9',
jg read_int_invalid # do something about it.
sub $48, %cl # Convert digit to integer
# Multiply the running total by 10, and then add the digit. That's the only real algorithm here.
mov -8(%ebp), %eax # put running total into %eax
mov $0, %edx # clear %edx to prepare for multiplication
mov $10, %ebx # can only multiply by a register
mul %ebx # ready, set, multiply!
add %ecx, %eax # add the digit on
mov %eax, -8(%ebp) # put this back in the local variable
jmp read_int_loop
read_int_invalid:
movl $1, -12(%ebp) # an invalid character means ignore the rest of the line
jmp read_int_loop
read_int_done:
pop %edi
pop %edx
pop %ecx
pop %ebx
mov -8(%ebp), %eax # return the running total in %eax
# clean up stack frame
mov %ebp, %esp
pop %ebp
ret
# My own subroutine. Prints a prompt (from the stack), then reads an int.
read_int_prompt:
push %ebp
mov %esp, %ebp
push 8(%ebp) # the prompt
call write
call read_int
mov %ebp, %esp
pop %ebp
ret
# This is the fun part of assembly programming.
sleep_for_eax_seconds:
# Actually, this is pretty complicated. The Linux call to sleep is nanosleep, which
# takes two pointers to structures as arguments. Luckily, the second one can be NULL.
# Unluckily, the first can't be NULL. I'll build one of these on the stack.
push %ebp
mov %esp, %ebp
pushal # basically all the registers need to be preserved
sub $8, %esp # make room for a structure
mov %eax, (%esp) # put seconds in first field
movl $0, 4(%esp) # zero second field
mov $162, %eax # nanosleep system call number
mov %esp, %ebx # address of the structure
mov $0, %ecx # second parameter is NULL
int $0x80 # let's hope it works...
add $8, %esp
popal
mov %ebp, %esp
pop %ebp
ret
# Tell vim that this is a GNU assembler file
/* vim: ft=gas :
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment