Assembling and Linking Code

There is a program called an assembler that translates from assembly-language code into executable instructions in binary: e.g. add $1, $2, $3 would be translated into 0x00430820. The first form of the instruction is usually written by a compiler but may be written by a human. It will be a line in a file called something like stuff.s. The .s at the end indicates that it is an assembly-language source file. The translated output will be part of a file with a name like stuff.o. The .o at the end of this name is there to indicate that it is an object file, one containing assembly output. (This use of the word object precedes object-oriented programming by several decades and is in no way related to it. It originally applied to a deck of cards whose creation was the object (i.e. the point) of running the assembler.) Assembly-language programs, like Java programs, are typically written in many files. Each of these files is assembled separately, and the result is one object file for each assembled source file. Once this is done, you have a bunch of object files. None of these files is runnable; after all none is a complete program. Not only that, each of them is assembled to begin at memory address zero. This means that they would all overlap if they were loaded into memory in that form. Clearly, this would not produce a working program.

There is another program, called a relocating linker whose purpose is to fix this problem for us. A relocating linker does two things: it relocates, and it links. We will talk about this in detail later. First we need to nail down the details of what the assembler does, but you do need to keep in mind that the link step will come along in time and fix up the problems left behind by the assembler. Things are done this way because it is not possible for the assembler to know enough when it is running to take care of everything. Each source file is assembled in isolation with the assembler having no knowledge of any other source files. The linker is the only program at work here that knows about all of the files involved.

Where SPIM is lacking

SPIM shows you the assembling of the source but not the linking. Every time you load a program into SPIM, you see both the source and object forms of each instruction in the text window. However, SPIM will not do linking. Every source file must be a complete program, unlike what happens in the real world. You see in the data and text windows of SPIM the result of both assembly and relocation. It is not what you would see if you looked at the output of a real assembler. The problem with this is that you don't get to see the relocation take place.

An Example Program

Just for purposes of illustration we will look at the following program and see what the assembler would do with it. It is our evenodd.s example that we have already looked at in class.

.data
x:      .word 1
y:      .word 2
evMesg: .asciiz "even"
odMesg: .asciiz "odd"
crlf:   .byte 13, 10, 0

.text
main:   lw $10, y
        andi $8, $10, 1
        beq $8, $0, even
        li $v0, 4
        la $a0, odMesg
        j print
even:   li $v0, 4
        la $a0, evMesg
print:  syscall
        jr $31

The Assembler Makes Two Passes Through Each Source File

On the first pass it creates a symbol table.

Our example program has a number of lines that are marked by labels. A label is a name followed by a colon character, and it will be at the beginning of its line. In the data segment we have the labels named x, y, evMesg, odMesg, and crlf. In the text segment we have main, even, and print. Each occurrence of a label defines a symbol. Each line of a symbol table contains a label name followed by the memory address of the assembled code for the line on which the label appeared and defined the symbol. The data and text segments have separate symbol tables. We will call these local symbol tables to distinguish them from global symbol tables, which will be described later. For our example, the symbol tables will be as follows:

x 0x0
y 0x4
evMesg 0x8
odMesg 0xD
crlf 0x11
main 0x0
even 0x20
print 0x2C
You can verify that these addresses are correct if you remember that each segment begins at zero and that some of the instructions have fake elements that each turn into two real instructions.

On the second pass it assembles the code and creates the object file.

Of course, the symbol tables created on the first pass will be used on the second. Look again at the example. You will see a number of places where a label name appears (without the colon) in an instruction, e.g. j print. Each such occurrence of a symbol name is called a reference to that symbol. Every time the assembler comes across a reference to a symbol, it looks that symbol up and uses the corresponding address, e.g. the j print of the previous example would be assembled as if it had been written j 0x2C.

Now the truth is, as you can see by the diagram on page A-20 of your book, that address zero is in the reserved area of memory. You already know from playing with SPIM that the data segment begins at address 0x10000000, and the text segment begins at address 0x00400000. These addresses in the assembler symbol tables are clearly temporary addresses. They will be replaced by the relocating linker with permanent addresses when the linker combines all of the object files into a single executable binary file.

The Relocating Linker Makes Two Passes Through All of the Object Files

On the first pass it relocates the object code and creates a global symbol table.

Relocation cures the problem of overlapping addresses that we mentioned before. The code from the various object files is laid out in adjacent blocks of memory so that there is no overlap. The data segment parts are put in the area of memory beginning at address 0x10000000, and the text segment parts in the area beginning at address 0x00400000. As an example, let's use three small text segments: the first uses the words at addresses 0, 4, 8; the second uses 0, and 4; and the third uses addresses 0, 4, 8, and C. The respective sizes of these three pieces are C, 8, and 10(hex). We would relocate them as shown below. Inside the column labeled "Detailed look" you see each piece expanded to show the individual words. Each word is represented by its address, and each address is given twice: once as an address local to that piece (the small numbers beginning at zero in each piece) and once as the address where the word actually ends up in memory (the big numbers that are consecutive throughout). We will call the first (small) numbers unrelocated addresses, and we will call the others relocated addresses. The local symbol tables use unrelocated addresses, and the global symbol table (see below) uses relocated addresses. Also in this column are some symbol names that we will use later.

Word Addresses Used Size Detailed look
other stuff 0x00400024..on up big
third piece 0x00400014..0x00400020 10
0x00400020 C
where 0x0040001C 8
0x00400018 4
there 0x00400014 0
second piece 0x0040000C..0x00400010 8
0x00400010 4
here 0x0040000C 0
first piece 0x00400000..0x00400008 C
0x00400008 8
0x00400004 4
main 0x00400000 0
other stuff 0x0..0x003FFFFC 0x00400000

 

First you should look at all the unrelocated addresses and verify that each piece is presented as originally described above. Now verify that the first relocated address in each piece is four more than the last one in the piece before it,

Now let's develop an algorithmic way to compute the relocated address of any word. The first relocated address in each piece will be called its base address. Notice first that the base address of the second piece is equal to the base address of the first piece plus the size of the first piece (0x0040000C = 0x00400000 + C). This generalizes: the base address of any piece (except the first) is equal to the base address of the previous piece plus the size of the previous piece. Next observe that the relocated address of any word is equal to its unrelocated address plus the base address of the piece that it is in (e.g. 0x0040001C = 0x00400014 + 8). Here is the algorithm for computing the base addresses of all the pieces:

int currentBase = 0x00400000;
while there are pieces to relocate
{
    get the next piece and call it the current piece
    base of the current piece = currentBase
    currentBase = currentBase + size of current piece
}

Now we need to construct a global symbol table. This contains all the symbols from the unrelocated symbol tables that we started with but with each address replaced by the corresponding relocated address. All that is necessary to do this is to add the unrelocated address to the base address of the piece it is in. But you already knew that from two paragraphs back.

Below are three local symbol tables that reflect the symbols shown in the previous table. In each case you can verify that the relocated address of the symbol is equal to the unrelocated address added to the base of the piece the symbol is in (e.g. where = 0x00400014 + 8 = 0x0040001C.

main 0
here 0
there 0
where 8
Here is the corresponding global symbol table:
main 0x00400000
here 0x0040000C
there 0x00400014
where 0x0040001C

Relocating creates some problems that need  to be solved

  1. Jump instructions use direct mode, so they contain the address that is to be jumped to (tricked up by times-4). When the instruction is relocated, those 26 bits need to be changed. As an exercise, you should determine what changes need to be made.
  2. References to symbols in the data segment need to be changed to reflect relocation of the symbol. Again, as an exercise, figure out the changes needed.
  3. What about branch instructions?

On the second pass it does the linking.

The Problem to Be Solved

Suppose that our main piece has a line that reads "jal there". The symbol there is located in a different piece, so the assembler will know nothing about it while it is assembling the main piece. This means that the assembler can't possibly put the correct 26 bits into the j-field of the instruction. Instead it puts in 26 zeroes and lets the linker fix this up later. Similarly an instruction like "lw $t5, x" where x is in the data segment of a different piece will have to be fixed up by the linker. In this case two instructions will have to be changed since the direct address mode in this instruction is a fake that has been unfaked by two real instructions.

The Solution

The linker uses the global symbol table to fix up the 26 bits to point to the right place. It simply looks up the symbol there, gets the address 0x00400014, divides it by 4, truncates to 26 bits, and puts them into the j-field of the instruction. If any of the bits lost to truncation are 1 bits, that is another problem. It can be fixed up, but we won't talk about that here.

Post-It Notes

The assembler inserts notes into the object files that identify the various parts of the code that need to be fixed up by the linker: These include relocation problems and linking problems. This is how the linker knows which instructions need fixing and what kind of fixing they need.