© Stephen Smith 2019
S. SmithRaspberry Pi Assembly Language Programminghttps://doi.org/10.1007/978-1-4842-5287-1_7

7. Linux Operating System Services

Stephen Smith1 
(1)
Gibsons, BC, Canada
 

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.

The calling convention is
  1. 1.

    r0r6: Input parameters, up to seven parameters for the system call.

     
  2. 2.

    r7: The Linux system call number (see Appendix B, “Linux System Calls”).

     
  3. 3.

    Call software interrupt 0 with “SVC 0”.

     
  4. 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

Many Linux services take pointers to blocks of memory as their parameters. The contents of these blocks of memory are documented with C structures, so as Assembly programmers, we have to reverse engineer the C and duplicate the memory structure. For instance, the nanosleep service lets your program sleep for a number of nanoseconds; it is defined as
int nanosleep(const struct timespec *req, struct timespec *rem);
and then the struct timespec is defined as
   struct timespec {
               time_t tv_sec;      /* seconds */
               long   tv_nsec;     /* nanoseconds */
           };
We then must figure out that these are two 32-bit integers, then define in Assembly
timespecsec:   .word   0
timespecnano:  .word   100000000
To use them, we load their address into the registers for the first two parameters:
        ldr         r0, =timespecsec
        ldr         r1, =timespecsec

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.

To start with, we need a library of file I/O routines to read from our input file, then write the uppercased version to another file. If you’ve done any C programming, these should look familiar, since the C runtime provides a thin layer over these services. We create a file fileio.s containing Listing 7-1 to do this.
@ Various macros to perform file I/O
@ The fd parameter needs to be a register.
@ Uses R0, R1, R7.
@ Return code is in R0.
.include "unistd.s"
.equ  O_RDONLY, 0
.equ  O_WRONLY, 1
.equ  O_CREAT,  0100
.equ  S_RDWR,   0666
.macro  openFile    fileName, flags
        ldr         r0, =fileName
        mov         r1, #flags
        mov      r2, #S_RDWR  @ RW access rights
        mov      r7, #sys_open
        svc         0
.endm
.macro  readFile   fd, buffer, length
        mov         r0, fd      @ file descriptor
        ldr         r1, =uffer
        mov         r2, #length
        mov         r7, #sys_read
        svc         0
.endm
.macro  writeFile   fd, buffer, length
        mov         r0, fd      @ file descriptor
        ldr         r1, =uffer
        mov         r2, length
        mov         r7, #sys_write
        svc         0
.endm
.macro  flushClose  fd
@fsync syscall
        mov         r0, fd
        mov         r7, #sys_fsync
        svc         0
@close syscall
        mov         r0, fd
        mov         r7, #sys_close
        svc         0
.endm
Listing 7-1

Macros to help us read and write files

Now we need a main program to orchestrate the process. We’ll call this main.s containing the contents of Listing 7-2.
@
@ Assembler program to convert a string to
@ all uppercase by calling a function.
@
@ R0-R2, R7 - used by macros to call linux
@ R8 - input file descriptor
@ R9 - output file descriptor
@ R10 - number of characters read
@
.include "fileio.s"
.equ  BUFFERLEN, 250
.global _start    @ Provide program starting address
_start:      openFile   inFile, O_RDONLY
      MOVS         R8, R0     @ save file descriptor
      BPL          nxtfil  @ pos number file opened ok
      MOV          R1, #1  @ stdout
      LDR          R2, =inpErrsz     @ Error msg
      LDR          R2, [R2]
      writeFile    R1, inpErr, R2 @ print the error
      B            exit
nxtfil: openFile   outFile, O_CREAT+O_WRONLY
      MOVS         R9, R0      @ save file descriptor
      BPL          loop    @ pos number file opened ok
      MOV          R1, #1
      LDR          R2, =outErrsz
      LDR          R2, [R2]
      writeFile    R1, outErr, R2
      B            exit
@ loop through file until done.
loop: readFile      R8, buffer, BUFFERLEN
      MOV           R10, R0     @ Keep the length read
      MOV           R1, #0      @ Null terminator for string
      @ set up call to toupper and call function
      LDR          R0, =buffer   @ first param for toupper
      STRB         R1, [R0, R10] @ put null at end of string.
      LDR          R1, =outBuf
      BL           toupper
      writeFile    R9, outBuf, R10
      CMP          R10, #BUFFERLEN
      BEQ          loop
      flushClose   R8
      flushClose   R9
@ Set up the parameters to exit the program
@ and then call Linux to do it.
exit: MOV     R0, #0      @ Use 0 return code
        MOV     R7, #1      @ Command code 1 terms
        SVC     0           @ Call linux to terminate
.data
inFile:  .asciz  "main.s"
outFile: .asciz    "upper.txt"
buffer:     .fill  BUFFERLEN + 1, 1, 0
outBuf:     .fill  BUFFERLEN + 1, 1, 0
inpErr: .asciz     "Failed to open input file. "
inpErrsz: .word  .-inpErr
outErr:     .asciz       "Failed to open output file. "
outErrsz: .word   .-outErr
Listing 7-2

Main program for our case conversion program

The makefile is contained in Listing 7-3.
UPPEROBJS = main.o upper.o
ifdef DEBUG
DEBUGFLGS = -g
else
DEBUGFLGS =
endif
LSTFLGS =
all: upper
%.o : %.s
      as $(DEBUGFLGS) $(LSTFLGS) $< -o $@
upper: $(UPPEROBJS)
      ld -o upper $(UPPEROBJS)
Listing 7-3

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

The Linux open service is typical of a Linux system service. It takes three parameters:
  1. 1.

    Filename: The file to open as a NULL-terminated string.

     
  2. 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. 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.

First of all, we have to copy the file descriptor to a register that won’t be overwritten, so we move it to R8. We do this with a MOVS instruction, so the CPSR will be set.
      MOVS      R8, R0      @ save file descriptor
This means we can test if it’s positive and if so go on to the next bit of code.
      BPL      nxtfil  @ pos number file opened ok
If the branch isn’t taken, then openFile returned a negative number. Here we use our writeFile routine to write an error message to stdout, then branch to the end of the program to exit.
      MOV         R1, #1  @ stdout
      LDR         R2, =inpErrsz  @ Error msg sz
      LDR         R2, [R2]
      writeFile   R1, inpErr, R2 @ print the error
      B           exit
In our .data section, we defined the error messages as follows:
inpErr: .asciz    "Failed to open input file. "
inpErrsz: .word  .-inpErr

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

In our loop, we
  1. 1.

    Read a block of 250 characters from the input file.

     
  2. 2.

    Append a NULL terminator.

     
  3. 3.

    Call toupper.

     
  4. 4.

    Write the converted characters to the output file.

     
  5. 5.

    If we aren’t done, branch to the top of the loop.

     
We check if we are done with
      CMP        R10, #BUFFERLEN
      BEQ        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 16 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.174.95