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:
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
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 _start: mov rax, sys_exit mov rdi, 0 syscall
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 ./a.out echo $?
These commands perform the following actions:
- The file "exit.asm" is assembled, producing an object file called "exit.o".
- The object file "exit.o" is linked, producing the executable file "a.out".
- The executable file "a.out" is run.
- 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 && ./a.out 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:
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
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
&& 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.
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 && ./a.out echo $?
It's exactly the same as before, except every occurrence of "exit" has been
$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:
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
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:
This version of the script is much more reusable. It can be used to assemble and run many simple programs without adjustment.