Baby's First x64 ASM Program: Advanced EMT Drug Calculations

Whenever I learn how to calculate a value for something useful -- even a simple one -- I like to write a program that performs the calculation for me. It helps me cement the process and recall it more easily. My Advanced EMT course has recently gone over medication-related skills like fluid boluses and injections. The calculations for these skills are very simple, but sometimes tricky to perform mentally on-the-fly. And, while 4 lines of Python will perform both of the calculations needed for these skills, I've been meaning to learn some assembly for a modern platform -- so I decided to pass on the Python this time. This post will explain the process and demo the resulting application.

Note: Although the article and code comments explain the intention of the code, I will not be going terribly in depth on how certain things work or why they're used the way they are. A couple good resources for learning more are Introduction to x86-64 Assembly for Compiler Writers and x64 Cheat Sheet. With that said, let's move on to building the program.

Step one: Hello World

As with any new language, printing something to the screen is a great way to see how a very simple program is built and structured. Performing this task in asm, however, is not exactly as simple as printf("Hello, world!\n");.

I'm developing with the GCC toolchain on linux, which defaults to the ugly AT&T syntax. So first, let's use a directive to allow us to use Intel syntax instead:

.intel_syntax noprefix

noprefix indicates that we won't be placing a % in front of every register name (for convenience and readability).

Next, we need to embed the string "Hello, world!" into the program. This is done by placing it in the read-only data section of the application:

   .section .rodata
   .string "Hello, world!\n"

Now, we need to define the function main, just like in C. There are a few steps to this, so I'll explain in the comments.

   .text            #This directive starts the section which contains executable code
   .globl main      #And this one declares main as a global symbol so it can be called from outside
   .type main, @function
   push rbp         #Save the previous base pointer. This instruction also happens to align the stack.
   mov rbp, rsp     #Load the base pointer from the stack pointer. The stack frame is set up now.
                    #Code goes here
   mov rsp, rbp     #Set the stack pointer back to the base pointer
   pop rbp          #Restore the previous base pointer

If we compile all of that with gcc -masm=intel helloworld.s... nothing happens. So let's take the last step: a function call to printf:

this goes inside main

mov rdi, OFFSET FLAT:.LC0   #Place the first argument (The string we defined) in rdi
mov rax, 0                  #Set the number of extra data arguments to 0
call printf

And now, if we run: gcc -masm=intel helloworld.s


the shell displays:

Hello, world!

Step two: Calculation functions

There are two very simple calculations this program needs to perform:

  1. Calculate the volume of a medication to inject given a weight based dose, a patient weight, the strength of the med in the vial, and the volume in the vial. This is calculated like so:

(target_dose * weight * vial_volume) / vial_strength

The program should be invoked like so:

./dc inj dose weight strength volume

  1. Calculate the required drip rate (in gtt/min) for a fluid bolus given a weight-based volume, a patient weight, the administration set used, and the duration over which to administer the bolus (in minutes). This is calculated as follows:

(volume * weight * drip_set) / minutes_to_infuse

For that, the program should be invoked like so:

./dc drip volume weight dripset minutes

For each of these calculations, we'll need a function. So let's start with the first:

   .type injection, @function
   sub rsp, 8                 #Align the stack
                              #Floating point values are passed in the xmm registers
   mulsd xmm0, xmm1           #Multiply dose by weight
   mulsd xmm0, xmm3           #Multiply resulting product by the volume in the vial
   divsd xmm0, xmm2           #Divide all of that by the strength of the med in the vial
   mov rdi, OFFSET FLAT:.LC2  #Set up printf. The first argument will be a string we'll set
                              #  up later to display the volume to be injected.
   mov rax, 1                 #We'll pass it one positional argument (the volume as a floating point value)
   call printf                #Display the output
   add rsp, 8                 #Reset the stack pointer to previous value

This function will calculate the volume of a drug to draw up, as long as we give it a string to output and make sure the arguments are set up in the floating point registers before-hand. We'll come back to that in a minute.

For now, let's get the drip rate function done:

   .type drip, @function
   sub rsp, 8                        #Align the stack before using call
   mulsd xmm0, xmm1                  #Multiply the volume by the weight
   mulsd xmm0, xmm2                  #Multiply that product by the dripset
   divsd xmm0, xmm3                  #Divide it all by the number of minutes
   roundsd xmm0, xmm0, 2             #Round up to integer
   cvtsd2si rsi, xmm0                #Convert the double to an int (no fractional gtt values)
   mov rdi, OFFSET FLAT:.LC3         #Print the output
   mov rax, 1
   call printf
   add rsp, 8                        #Reset stack pointer to previous value

Like the first, this function will work as long as it has a string to output and the arguments set up in advance. So, let's figure out how to set up the arguments. Each argument will be passed in as a string -- or, more accurately, a pointer to an array of char values -- however, we need them to be floating point values instead. Later on, in main, we'll parse the arguments and place them on the stack.

Next, let's write a function that removes them from the stack and places them into the correct registers for our above functions:

   .type setup_fp_args, @function
   movsd xmm0, [rbp-8]      #Start at the *second* variable on the stack. The first will be used for something else.
   movsd xmm1, [rbp-16]
   movsd xmm2, [rbp-24]
   movsd xmm3, [rbp-32]

We need one more function before we can start putting it all together: one that prints usage information if the arguments passed are inappropriate. This one is pretty easy:

   .type argerror, @function
   sub rsp, 8                        #This aligns the stack before we use call
   mov rdi, OFFSET FLAT:.LC4
   mov rax, 0
   call printf
   add rsp, 8                        #Fix the stack before returning

and that brings us to...

Step three: Parsing command line arguments

With the command line arguments, there are three tasks to be performed:

  1. Make sure there are the correct number of them (6)
    • the name of the program
    • 'inj' or 'drip' to indicate which calculation to perform
    • and 4 floating point values to pass into either function
  2. Parse the number strings into floating point values
  3. Parse the 'inj' or 'drip' argument to decide which calculation is to be performed

Here is the code (inside main), with some explanation in the comments:

   cmp rdi, 6                        #Check that there are 6 command line arguments
   je skip_argerror          #If there are, don't call argerror
   call argerror                     #Otherwise, call argerror and exit
   jmp clean_up
   sub rsp, 40                       #Allocate space for 5 local variables
   movb [rbp], 0                     #The first one keeps track of whether we've called
                                     #       one of our calculation functions
   mov rbx, rsi                      #Save the value of rsi so we can use it to pass arguments
   mov rdi, QWORD PTR [rbx+16]       #Call strtod with the third command line arg
   mov rsi, 0
   call strtod
   movsd [rbp-8], xmm0               #Save it in the next local variable

Repeat the call to strtod with each argument, saving them on the stack in order.


Finally, we'll parse for 'inj' or 'drip', call the appropriate function, and then clean up and exit the program:

   mov rdi, OFFSET FLAT:.LC0         #Check if the second argument was 'inj'
   mov rsi, QWORD PTR [rbx+8]
   mov rdx, 3
   call memcmp
   cmp rax, 0
   jne skip_injection
   call setup_fp_args                #Load the arguments into xmm registers to pass to injection
   call injection
   movb [rbp], 1                     #Set the first local to 1 since we called injection
   mov rdi, OFFSET FLAT:.LC1         #Check if the second argument was 'drip', and do the same as
                                     #       above
   mov rsi, QWORD PTR [rbx+8]
   mov rdx, 4
   call memcmp
   cmp rax, 0
   jne skip_drip
   call setup_fp_args
   call drip
   movb [rbp], 1
   cmpb [rbp], 0                     #Check if we called one of our calculation functions.
                                     #       If so, then just clean_up and exit
   jne clean_up
   call argerror                     #Otherwise, call argerror
   add rsp, 40
   pop rbx
   mov rsp, rbp
   pop rbp

Step four: odds and ends

Each of the functions above references a string to be passed to printf or memcmp. So the last piece we need is to define those strings. This was done like so:

   .section .rodata
   .string "inj"
   .string "drip"
   .string "Administer %f mL\n"
   .string "Set drip rate to %d gtt/min\n"
   .string "Usage for calculating injection volumes: ./dc inj [dose] [weight in kg] [stock strength] [stock volume]\n\n or for drip rates: \n\n./dc drip [volume per kilo] [weight in kg] [dripset] [minutes]\n"

The final product

The full source for my program can be viewed here.

Usage Demo:

echodrop@debian:~/dev/asm$ ./dc inj 2, 59, 50, 1
Administer 2.360000 mL
echodrop@debian:~/dev/asm$ ./dc drip 20, 59, 10, 240
Set drip rate to 50 gtt/min