Run script

Before continuing with more programming, we should optimize the process a bit. Previously, every time a change has been made to the program, it's been necessary to run the assembler, the linker, and finally run the program. In order to see the exit status code, yet another command is necessary: echo $?. This process can be improved. A fast feedback loop can be very helpful, allowing you to quickly see the results of changes as you experiment with different instructions.

For real software projects, there are a number of proper build tools available. The GNU program make is very popular and is a fairly standardized way to manage complex builds. For these small exercises, however, a quicker way to get up and running is to use a shell script.

Any commands you type into a UNIX terminal emulator can be scripted. This includes all of the commands we've used so far to assemble, link, and run programs.

By creating a file which contains all of the commands to be run in order, you can type a single command into the terminal, which will automatically run that list of commands, saving you the chore of re-entering them over and over every time you make a change to your program.

A new program

First, open a new file and call it "exit.asm". Type the following and save it:

%define sys_exit 60

section .text

global _start
	mov rax, sys_exit
	mov rdi, 0

This should look pretty familiar: it's basically the previous program but without the console print system call. All this program does is immediately exit, returning a status code of 0.

Writing the run script

Now we assemble and run it. The manual way to do this looks something like the following:

nasm -f elf64 exit.asm
ld exit.o
echo $?

These commands perform the following actions:

  1. The file "exit.asm" is assembled, producing an object file called "exit.o".
  2. The object file "exit.o" is linked, producing the executable file "a.out".
  3. The executable file "a.out" is run.
  4. Finally, the status code of the program is printed to the console.

This manual procedure works fine, but it slows down the process of programming. It's important to have a short feedback loop so that when you make changes, you can instantly see the result. To this end, we're going to turn these commands into a shell script which will perform them automatically.

Open a new file and call it "run". Type the following and save it:

#!/usr/bin/env bash

nasm -f elf64 exit.asm &&
ld exit.o &&
echo $?

This looks very similar to the manual list of commands above, but with a few changes. We'll step through this one line at a time:

#!/usr/bin/env bash

Since this is a text file instead of machine code, the computer can't execute it directly. This first line tells the computer which program to use to interpret the rest of the file.

When you attempt to execute a text file, the first line of that file is checked to figure out how to process the file. The first two characters #! are called a shebang, and they signal that the text to follow will be a program capable of interpreting the rest of the file. This can be any program, provided it knows how to interpret the contents of the file. Python and Perl are common examples, but you can also make your own. In this case, it's bash, which is a shell interpreter. This means bash will interpret the rest of the contents of the file as if they were commands entered on the terminal.

nasm -f elf64 exit.asm &&

This command runs the nasm assembler on the "exit.asm" file. This should be familiar from the last section. However, there is also something new: the double ampersand (&&) at the end. This means that the script should only continue if this command is successful. If nasm fails to assemble the program (for example, if there's a syntax error in "exit.asm"), the script will not continue.

ld exit.o &&

This runs the linker to create the final executable file "a.out". Again, the && makes sure the script will only continue if this step is successful.


Now the program is run. Notice that this line doesn't end with a double ampersand && like the previous lines. This is because our program may not exit successfully. Remember that when a program ends, it returns a status code. If that status code is 0, the program is considered successful. If the status code is not 0, it's considered a failure. If our program doesn't return a 0, we still want the next line to run, so that we can see the status code returned. If we used && at the end of this line, we would only see the status code returned by our program if it was a 0. This way, the script will continue regardless of the status code returned by the program.

echo $?

This line prints the exit status code of the previous program, which in this case is our program: "a.out".

Running the script

Make sure both "exit.asm" and "run" are typed correctly and saved to disk. Then, using the terminal, make the "run" script executable:

chmod +x run

This shell command makes the "run" text file executable. Without it, the shell interpreter would refuse to run it.

Finally, execute the "run" script:


If all went well, you should see the number "0". This means the "exit.asm" program was assembled, linked, executed, and returned a status code of 0. To confirm, try changing the "exit.asm" program to return a different status code.

Change the line:

	mov rdi, 0


	mov rdi, 7

And then run the script again:


You should now see a 7 instead of a 0. The "run" script should speed up your ability to experiment and try different things as you go along. It can be adapted to each project by changing the names of the files in the commands.

Making the script more reusable

Right now the script uses hard-coded filenames. It works with the "exit.asm" program, but if you wanted to use it on another file, you'd have to replace all occurrences of the word "exit" with the new program name. It's possible to have this replacement performed automatically every time the script is run by using variables.

Take a look at the final version of the script below:

#!/usr/bin/env bash

nasm -f elf64 $1.asm &&
ld $1.o &&
echo $?

It's exactly the same as before, except every occurrence of "exit" has been replaced with $1. In bash, $1 refers to the first command-line argument passed in when the script was run. Make the changes above and then try running the script again, this time specifying "exit" on the command line:

./run exit

This command executes the "run" script, passing it a single command-line argument with a value of "exit". From within the "run" script, the symbol $1 refers to the first command-line argument. So every time $1 appears in the "run" script, it will be replaced with whatever you type after ./run on the command line: in this case, "exit".

The script will otherwise work the same as before, but now you can use it to run other programs just by replacing "exit" with the name of the program you want to assemble and run. So if you made another program called "something.asm", you could assemble, link, and run it by entering:

./run something

This version of the script is much more reusable. It can be used to assemble and run many simple programs without adjustment.

Next section: User input