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:
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:
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 main: 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 ret
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:
Step two: Calculation functions
There are two very simple calculations this program needs to perform:
- 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
- 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 injection: 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 ret
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 drip: 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 ret
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 setup_fp_args: 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] ret
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 argerror: 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 ret
and that brings us to...
Step three: Parsing command line arguments
With the command line arguments, there are three tasks to be performed:
- 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
- Parse the number strings into floating point values
- 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 skip_argerror: 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 skip_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 skip_drip: 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 clean_up: add rsp, 40 pop rbx mov rsp, rbp pop rbp ret
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 .LC0: .string "inj" .LC1: .string "drip" .LC2: .string "Administer %f mL\n" .LC3: .string "Set drip rate to %d gtt/min\n" .LC4: .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.