In Chapter 1, “Getting Started,” we needed the ability to exit our program and to display a string. We used Raspbian Linux to do this, invoking operating system services directly. In all high-level programming languages, there is a runtime library that includes wrappers for calling the operating system. This makes it appear that these services are part of the high-level language. In this chapter, we’ll be looking at what these runtime libraries do under the covers to call Linux and what services are available to us.
We will review the syntax for calling the operating system and the error codes returned to us. There is a complete listing of all the services and error codes in Appendix B, “Linux System Calls.”
So Many Services
If you look at Appendix B, “Linux System Calls,” it looks like there are nearly 400 Linux system services. Why so many? Linux turned 25 years old in 2019. That’s quite old for a computer program. These services were added piece by piece over all those years. The problem of this patchwork development arises in software compatibility. If a service call requires a parameter change, then the current service can’t be changed without breaking a bunch of programs.
The solution to software incompatibility is often to just add a new function. The old function then becomes a thin wrapper that translates the parameters to what the new function requires. Examples of this are any file access routines that take an offset into a file or a size parameter. Originally, 32-bit Linux only supported files 32 bits in length (4 GB). This became too small, and a whole new set of file I/O routines were added that take a 64-bit parameter for file offsets and sizes. All these functions are like the 32-bit versions, but with 64 appended to their names.
Fortunately, the Linux documentation for all these services is quite good. It is oriented entirely to C programmers, so anyone else using it must know enough C to convert the meaning to what is appropriate for the language they are using.
Linux is a powerful operating system—as an application or systems programmer, it certainly will help you learn Linux system programming. There are a lot of services to help you. You don’t want to be reinventing all these yourself, unless you are creating a new operating system.
Calling Convention
We’ve used two system calls: one to write ASCII data to the console and the second to exit our program. The calling convention for system calls is different than that for function. It uses a software interrupt to switch context from our user-level program to the context of the Linux kernel.
- 1.
r0–r6: Input parameters, up to seven parameters for the system call.
- 2.
r7: The Linux system call number (see Appendix B, “Linux System Calls”).
- 3.
Call software interrupt 0 with “SVC 0”.
- 4.
R0: The return code from the call (see Appendix B, “Linux System Calls”).
The software interrupt is a clever way for us to call routines in the Linux kernel without knowing where they are stored in memory. It also provides a mechanism to run at a higher security level while the call executes. Linux will check if you have the correct access rights to perform the requested operation and give back an error code like EACCES (13) if you are denied.
Although it doesn’t follow the function calling convention from Chapter 6, “Functions and the Stack,” the Linux system call mechanism will preserve all registers not used as parameters or the return code. When system calls require a large block of parameters, they tend to take a pointer to a block of memory as one parameter, which then holds all the data they need. Hence, most system calls don’t use that many parameters.
The return code for these functions is usually zero or a positive number for success and a negative number for failure. The negative number is the negative of the error codes in Appendix B, “Linux System Calls.” For example, the open call to open a file returns a file descriptor if it is successful. A file descriptor is a small positive number, then a negative number if it fails, where it is the negative of one of the constants in Appendix B, “Linux System Calls.”
Structures
We’ll be using the nanosleep function in Chapter 8, “Programming GPIO Pins,” but this is typical of what it takes to directly call some Linux services.
Wrappers
Rather than figure out all the registers each time we want to call a Linux service, we will develop a library of routines or macros to make our job easier. The C programming language includes function call wrappers for all the Linux services; we will see how to use these in Chapter 9, “Interacting with C and Python.”
Rather than duplicate the work of the C runtime library, we’ll develop a library of Linux system calls using the GNU Assembler’s macro functionality. We won’t develop this for all the functions, just the functions we need. Most programmers do this, then over time their libraries become quite extensive.
A problem with macros is that you often need several variants with different parameter types. For instance, sometimes you might like to call the macro with a register as a parameter and other times with an immediate value.
Converting a File to Uppercase
In this chapter, we present a complete program to convert the contents of a text file to all uppercase. We will use our toupper function from Chapter 6, “Functions and the Stack,” and get practice coding loops and if statements.
Macros to help us read and write files
Main program for our case conversion program
Makefile for our file conversion program
This program uses the upper.s file from Chapter 6, “Functions and the Stack,” that contains the function version of our uppercase logic. The program also uses the unistd.s from Appendix B, “Linux System Calls,” that gives meaningful definitions of the Linux service function numbers.
If you build this program, notice that it is only 13 KB in size. This is one of the appeals of pure Assembly language programming. There is nothing extra added to the program—we control every byte—no mysterious libraries or runtimes added.
Note
The files this program operates on are hard-coded in the .data section. Feel free to change them, play with them, generate some errors to see what happens. Single-step through the program in gdb to ensure you understand how it works.
Opening a File
- 1.
Filename: The file to open as a NULL-terminated string.
- 2.
Flags: To specify whether we’re opening it for reading or writing or whether to create the file. We included some .EQU directives with the values we need (using the same names as in the C runtime).
- 3.
Mode: The access mode for the file when creating the file. We included a couple of defines, but in octal these are the same as the parameters to the chmod Linux command.
The return code is either a file descriptor or an error code. Like many Linux services, the call fits this in a single return code by making errors negative and successful results positive.
Error Checking
Books tend to not promote good programming practices for error checking. The sample programs are kept as small as possible, so the main ideas being explained aren’t lost in a sea of details. This is the first program where we test any return codes. Partly, we had to develop enough code to be able to do it, and second error checking code tends to not reveal any new concepts.
File open calls are prone to failing. The file might not exist, perhaps because we are in the wrong folder, or we may not have sufficient access rights to the file. Generally, check the return code to every system call, or function you call, but practically programmers are lazy and tend to only check those that are likely to fail. In this program, we check the two file open calls.
We’ve seen .asciz and this is standard. For writeFile, we need the length of the string to write to the console. In Chapter 1, “Getting Started,” we counted the characters in our string and put the hard-coded number in our code. We could do that here too, but error messages start getting long and counting the characters seems like something the computer should do. We could write a routine like the C library’s strlen() function to calculate the length of a NULL-terminated string. Instead, we use a little GNU Assembler trickery. We add a .word directive right after the string and initialize it with “.-inpErr”. The “ . ” is a special Assembler variable that contains the current address the Assembler is on as it works. Hence, the current address right after the string minus the address of the start of the string is the length. Now people can revise the wording of the error message to their heart’s content without needing to count the characters each time.
Most applications contain an error module, so if a function fails, the error module is called. Then the error module is responsible for reporting and logging the error. This way, error reporting can be made quite sophisticated without cluttering up the rest of the code with error-handling code. Another problem with error-handling code is that it tends to not be tested. Often bad things can happen when an error finally does happen, and problems with the previously untested code manifest.
Looping
- 1.
Read a block of 250 characters from the input file.
- 2.
Append a NULL terminator.
- 3.
Call toupper.
- 4.
Write the converted characters to the output file.
- 5.
If we aren’t done, branch to the top of the loop.
R10 contains the number of characters returned from the read service call. If it equals the number of characters requested, then we branch to loop. If it doesn’t equal exactly, then either we hit end of file, so the number of characters returned is less (and possibly 0), or an error occurred, in which case the number is negative. Either way, we are done and fall through to the program exit.
Summary
In this chapter, we gave an overview of how to call the various Linux system services. We covered the calling convention and how to interpret the return codes. We didn’t cover the purpose of each call and referred the user to the Linux documentation instead.
We presented a program to read a file, convert it to uppercase, and write it out to another file. This is our first chance to put together what we learned in Chapters 1–6 to build a full application, with loops, if statements, error messages, and file I/O.
In the next chapter, we will use Linux service calls to manipulate the GPIO pins on the Raspberry Pi board.