In this chapter, we will learn a better way to build our programs using GNU Make. With the GNU Debugger (GDB), we will debug our programs. We’ll look at the tools required to cross-compile for ARM from an Intel computer, develop Assembly Language for Google Android, and add Assembly Language to Apple iOS apps. Also, we will quickly introduce the source control system Git and the build server Jenkins.
GNU Make
- 1.
Specifies the rules on how to build one thing from another
- 2.
Lists the targets you want built and the files they depend on
- 3.
Examines the file date/times to determine what needs to be built
- 4.
Issues the commands to build the components
Simple makefile for HelloWorld
The command make is particular, and the indented lines must start with a tab not spaces, or you will get an error.
Rebuilding a File
Rather than specify each file separately along with the command to build it, we can define a build rule for, say, building a .o file from an .s file.
A Rule for Building .s Files
Hello World makefile with a rule
%.s is like a wildcard meaning any .s file.
$< is a symbol for the source file.
$@ is a symbol for the output file.
There’s a lot of good documentation on make, so we aren’t going to go into a lot of detail here.
Defining Variables
Adding a variable to the Hello World makefile
With this code, as we add source files, we just add the new file to the OBJS= line and make takes care of the rest.
This is just an introduction to GNU Make—there is a lot more to this powerful tool. As we go further into the book, we will introduce new elements to our makefiles as needed.
GDB
Most high-level languages come with tools to easily output any strings or numbers to the console, a window, or a web page. Often when using these languages, programmers don’t bother using the debugger, instead relying on libraries that are part of the language.
Later, we’ll look at how to leverage the libraries that are part of other languages, but calling these takes a bit of work. We’ll also develop a helpful library to convert numbers to strings, so we can use the techniques used in the HelloWorld program in Chapter 1, “Getting Started,” to print our work.
When programming with Assembly Language, being proficient with the debugger is critical to success. Not only will this help with your Assembly Language programming, but also it is a great tool for you to use with your high-level language programming.
Preparing to Debug
The GNU Debugger (GDB) can debug your program as it is, but this isn’t the most convenient way to go. For instance, in our HelloWorld program, we have the label helloworld. If we debug the program as is, the debugger won’t know anything about this label, since the Assembler changed it into an address in a .data section. There is a command line option for the Assembler that includes a table of all our source code labels and symbols, so we can use them in the debugger. This makes our program executable a bit larger.
Often, we set a debug flag while we are developing the program, then remove the debug flag before releasing the program. Unlike some high-level programming languages, the debug flag doesn’t affect the machine code generated, so the program behaves exactly the same in both debug and non-debug mode.
We don’t want to leave the debug information in our program for release, because besides making the program executable larger, it is a wealth of information for hackers to help them reverse engineer your program. There are several cases where hackers caused mischief because the program still had debugging information present.
Makefile with a debug flag
When switching between DEBUG and non-DEBUG, run make with the -B switch to build everything.
Create shell scripts buildd and buildr to call make with and without DEBUG defined.
Beginning GDB
gdb is a command line program.
(gdb) is the command prompt where you type commands.
(hit tab) for command completion. Enter the first letter or two of a command as a shortcut.
(or r).
The program runs to completion, as if it ran normally from the command line.
(or l).
to list our entire program.
This shows the actual code produced by the Assembler with no comments. We can see whether MOV or MVN were used among other commands this way.
We see 0x6E3A put in X2 as expected.
N is the number of objects to display
- f is the display format where some common ones are
t for binary
x for hexadecimal
d for decimal
i for instruction
s for string
- u is unit size and is any of
b for bytes
h for halfwords (16 bits)
w for words (32 bits)
g for giant words (64 bits)
To exit gdb, type q (for quit or type control-d).
Summary of useful GDB commands
Command (Short Form) | Description |
---|---|
break (b) line | Set breakpoint at line |
run (r) | Run the program |
step (s) | Single step program |
continue (c) | Continue running the program |
quit (q or control-d) | Exit gdb |
control-c | Interrupt the running program |
info registers (i r) | Print out the registers |
info break | Print out the breakpoints |
delete n | Delete breakpoint n |
x /Nuf expression | Show contents of memory |
It’s worthwhile single stepping through our three sample programs and examining the registers at each step to ensure you understand what each instruction is doing.
Even if you don’t know of a bug, many programmers like to single step through their code to look for problems and to convince themselves that their code is good. Often two programmers do this together as part of the pair programming agile methodology.
Cross-Compiling
So far, we’ve been compiling and running our programs on an ARM-based computer like the Raspberry Pi or NVidia Jetson Nano; however, we can also compile and run our programs on an Intel-based computer. In this section, we’ll see how to compile and run the Hello World program from Chapter 1, “Getting Started,” on Ubuntu Linux running on an Intel-based laptop.
The GNU Assembler and the Linux linker/loader are both open source programs and can be compiled to run on any system. The GNU Assembler source code contains support for many CPU architectures, and the code it is written in compiles on all sorts of systems. Ubuntu Linux on Intel comes with all the GNU tools installed, but they compile Intel Assembly Language code instead of ARM. It would be nice if the GNU Assembler had a command line switch to tell it to compile ARM code, but that isn’t how it works. You need to specify the type of Assembly code to process at compile time.
Makefile to build Hello World on an Intel CPU
We’ve now built our Hello World program for ARM on an Intel CPU. This is called cross-compiling. This is most used when programming embedded ARM processors that don’t run a full Linux kernel and hence don’t have all the development tools available. The workflow is to build the program on a full development system and then transfer the program to the target processor using a USB cable, serial cable, or via Ethernet. You can copy the resulting program to a Raspberry Pi or NVidia Jetson computer to run it. Even if your target platform supports all the development tools, it can be faster to do your builds on a more powerful laptop or desktop.
Emulation
We have now successfully compiled and run our ARM 64-bit Assembly Language program on an Intel PC.
Android NDK
To run our HelloWorld program from Chapter 1, “Getting Started,” is surprisingly easy. This is because Android is based on Linux, and as time has gone by, Google has moved Android closer and closer to standard Linux. The main thing we need to do is install the official tools, compile our program, and copy it over to an Android device to run. You can’t develop for Android on an ARM-based system like a Raspberry Pi or an Android-based laptop; you must develop on an Intel system under either Linux, MacOS, or Windows.
Not all Android devices are based on ARM CPUs. Ensure your Android device contains an ARM CPU and that you are running a 64-bit version of Android.
As you can see, these will move as the NDK or Android is updated to new version. This is like what we did when cross-compiling; only the tools have separate names, namely, aarch64-linux-android-as and aarch64-linux-android-ld. Since the commands have unique names, we can add the preceding path to our system PATH in our .bashrc file without conflicting with our system’s default applications.
Makefile to build HelloWorld for Android
to build our program for Android.
We now have our HelloWorld program built for Android but sitting on our Intel-based laptop. How do we copy it to our device and run it? Android is a locked down version of Linux and expects people to only run programs downloaded from the Google Play Store. To run our programs, we need to put the Android device into developer mode. This is usually accomplished by tapping on the build number in the settings menu multiple times. Once the device is in developer mode, a developer menu will be added to the settings menu; from here, we need to enable USB debugging. I find it convenient to disable sleep mode while charging as well.
This demonstrates how learning Assembly Language for Linux can be directly leveraged to incorporate Assembly Language into an Android program. Android developers develop apps and not command line programs; in Chapter 9, “Interacting with C and Python,” we’ll create a true Android app and make an Assembly Language routine do some processing.
Apple XCode
All up-to-date Apple iPhones and iPads run a 64-bit version of iOS and utilize an ARM processor. All iOS apps are written in Objective-C or Swift; however, Apple’s XCode development environment does have support for incorporating Assembly Language code. In this section, we’ll look at how to run our Hello World program from Chapter 1, “Getting Started,” on either an iPhone or iPad.
To run the program in this section, you are required to have a Mac laptop or desktop running an up-to-date version of MacOS. However, if you aren’t interested in developing for iOS, you can skip this section. You also will need an iPhone or iPad to run the program on.
iOS is based on NeXTSTEP which is based on Berkeley Unix (BSD), not Linux, so things will be different than what we’ve seen so far. However, iOS does incorporate the POSIX Unix standard which Linux also supports. The result is that the changes required to make our Hello World program work on an iOS device are surprisingly minor.
iOS is a regulated environment, so we can’t just open a terminal window and run our programs from the command line. We need to create an “official” iOS app, code sign it, and then download it from our Mac to our iOS device. We’ll create an empty Objective-C project, add our Hello World file, and pass control to our program.
Apple iOS HelloWorld.s
- 1.
The operating system function number is placed in register X16 rather than X8.
- 2.
iOS uses software interrupt 0x80 rather than 0 to make the operating system call.
- 3.
The function numbers are different. These are the same function numbers used in 32-bit Linux. When iOS went from 32 to 64 bits, Apple kept the operating system function numbers the same, whereas Linux rearranged them completely.
- 4.
We use “adr X1, helloworld” rather than “ldr X1,=helloworld” to load the address of our string (also note we don’t have a .data section). We’ll discuss the difference between these in Chapter 5, “Thanks for the Memories”; for now, it is just two different ways to get the address of our string loaded into register X1. We had to make this switch since iOS prohibits the previous method.
Otherwise, this should all look very familiar.
ViewController.m
We call start(), rather than _start(), because the Objective-C compiler will “decorate” the function name adding the “_”. Now we are ready to run.
If we just select project build at this point, we will get a large number of cryptic error messages from the Assembler. This is because by default, XCode will try to run our program in one of the iOS simulators on the Mac. Normally this is fine, but it won’t work for any app containing Assembly Language code. This is because the Mac uses an Intel processor, and to compile for the simulator, XCode will try to interpret our HelloWorld.s file as Intel Assembly language, which it isn’t.
I left out any steps to initialize your device or set up your developer id with Apple. These are all necessary, but if you are doing iOS development, these should already have been completed.
Be careful with Assembly Language programming on iOS as if you do something that Apple doesn’t like, they will remove you from the App Store.
This section was just to give you an idea of how to add Assembly Language to an iOS app. It isn’t a realistic example, especially since it terminates the program. Typically, you write Assembly Language to implement fast functions called from the high-level language. We’ll cover how to implement functions, including taking parameters and returning values in Chapter 9, “Interacting with C and Python.”
Source Control and Build Servers
Although make is fine for our purposes in this book, there are much more sophisticated build systems. As your programs get larger, managing changes and versions becomes more challenging; to help with this, there are version control systems like Git. The source code for this book is hosted on a cloud version of Git, called GitHub. You can get a link to this book’s source code from this book’s web page on Apress.com.
Git
As your program gets larger, consider using a source control system to manage source files. Source control systems keep all the versions of your program. With source control, it’s easy to retrieve the files that make up version 1.15 of your program; you can have multiple branches, so you can work on both version 1.16 while also working on version 2.1 and keep everything straight.
Once you have a team of programmers working on your project, you need to regulate who is editing what, so people don’t overwrite each other’s work. Git takes this to a new level, where two people can edit the same file; then Git can merge the changes to keep both people’s work. Git is a great program for doing this. Git was developed by Linus Torvalds as the source control system for all Linux development. There are cloud versions, like GitHub, that keep your files in the Cloud, and as a result, you don’t need to worry about backing them up.
The SD Cards, the Raspberry Pi, and NVidia Jetson use instead of hard drives or SSDs are not as reliable. They can fail, so you should always have a backup of your work. If you don’t back up to the Cloud with a service like GitHub, back up with one of the following:
Copy your files to Google Drive.
E-mail your files to yourself.
Copy them to a USB hard drive.
Don’t trust the SD Card, as it will fail at some point.
Git is a sophisticated system beyond the scope of this book, but worth checking out.
Jenkins
Once you are using GNU Make and Git, you might consider checking out Jenkins. Jenkins is a build server that monitors Git, and every time you check in a new version of a program file, it kicks off a build. This is part of a continuous development system that can even deploy your program.
This is especially helpful if you have a team of programmers, where the build takes a long time, or you need the result to automatically be deployed, say, to a web server.
If you have a set of automated tests, these are run after each build. Having the automated tests run frequently helps you detect when your program is broken. The cost of fixing a bug tends to be proportional to the time that the bug exists in the code, so finding and fixing bugs quickly is a huge productivity gain.
Summary
In this chapter, we introduced the GNU Make program that we will use to build our programs. This is a powerful tool used to handle all the rules for the various compilers and linkers we need.
We then introduced the GNU Debugger that will allow us to troubleshoot our programs. Unfortunately, programs have bugs and we need a way to single step through them and examine all the registers and memory as we do so. GDB is a technical tool, but it’s indispensable in figuring out what our programs are doing.
We covered how to cross-compile our code on Intel-based computers and how to run our ARM programs in an emulator. We then covered how to set up an Android development environment for Assembler development and run our HelloWorld program on an Android device. We then covered how to create an Apple iOS app and run a modified version of our HelloWorld program on an iPad or iPhone.
Lastly, we mentioned the source control system Git and the build server Jenkins. We won’t be using these in this book, but as your needs get more sophisticated, you should check these out.
In Chapter 4, “Controlling Program Flow,” we will look at conditionally executing code, branching, and looping—the core building blocks of programming logic.
Exercises
- 1.
Create a makefile for one of the small programs in Chapter 2, “Loading and Adding.”
- 2.
Step through the small program from Chapter 2, “Loading and Adding,” to ensure you understand the changes each instruction makes to the registers.
- 3.
If you have a computer with an Intel processor, set it up to cross-compile for ARM and compile HelloWorld. Install the emulator and run it on the Intel computer.
- 4.
If you have an ARM-based Android 64-bit device and an Intel computer, set it up for Android Assembly development and run HelloWorld.
- 5.
If you have a Mac and iPad or iPhone, install XCode and compile and run HelloWorld as indicated.