The von Neumann Model
ASCII TEMPLATE
6.1 Problem Solving
Programming
We are now ready to start developing programs to solve problems with the com-puter. In this chapter we attempt to do two things: first, we develop a methodology for constructing programs; and second, we develop a methodology for fixing those programs under the likely condition that we did not get it right the first time. There is a long tradition that the errors present in programs are referred to as bugs, and the process of removing those errors debugging. The opportunities for introduc-ing bugs into a complicated program are so great that it usually takes much more time to get the program to work (debugging) than it does to create it in the first place.
6.1 Problem Solving
6.1.1 Systematic Decomposition
Recall from Chapter 1 that in order for electrons to solve a problem, we need to go through several levels of transformation to get from a natural language description of the problem (in our case English, although some of you might prefer Italian, Mandarin, Hindi, or something else) to something electrons can deal with. Once we have a natural language description of the problem, the next step is to transform the problem statement into an algorithm. That is, the next step is to transform the problem statement into a step-by-step proce-dure that has the properties of finiteness (it terminates), definiteness (each step is precisely stated), and effective computability (each step can be carried out by a computer).
156 chapter 6 Programming
In the late 1960s, the concept of structured programming emerged as a way to improve the ability of average programmers to take a complex description of a problem and systematically decompose it into sufficiently smaller, manageable units that they could ultimately write as a program that executed correctly. The mechanism has also been called systematic decomposition because the larger tasks are systematically broken down into smaller ones.
We will find the systematic decomposition model a useful technique for designing computer programs to carry out complex tasks.
6.1.2 The Three Constructs: Sequential, Conditional, Iterative
Systematic decomposition is the process of taking a task, that is, a unit of work (see Figure 6.1a), and breaking it down into smaller units of work such that the collection of smaller units carries out the same task as the one larger unit. The idea is that if one starts with a large, complex task and applies this process again and again, one will end up with very small units of work, and consequently, be able to easily write a program to carry out each of these small units of work. The process is also referred to as stepwise refinement, because the process is applied one step at a time, and each step refines one of the tasks that is still too complex into a collection of simpler subtasks.
The idea is to replace each larger unit of work with a construct that correctly decomposes it. There are basically three constructs for doing this: sequential, conditional, and iterative.
(a) y
The task to be decomposed
v
(b) (c) (d) Sequential Conditional Iterative
Figure 6.1 The basic constructs of structured programming
The sequential construct (Figure 6.1b) is the one to use if the designated task can be broken down into two subtasks, one following the other. That is, the computer is to carry out the first subtask completely, then go on and carry out the second subtask completely—never going back to the first subtask after starting the second subtask.
The conditional construct (Figure 6.1c) is the one to use if the task consists of doing one of two subtasks but not both, depending on some condition. If the condition is true, the computer is to carry out one subtask. If the condition is not true, the computer is to carry out a different subtask. Either subtask may be vacuous, that is, it may "do nothing." Regardless, after the correct subtask is completed, the program moves onward. The program never goes back and retests the condition.
The iterative construct (Figure 6. Id) is the one to use if the task consists of doing a subtask a number of times, but only as long as some condition is true. If the condition is true, do the subtask. After the subtask is finished, go back and test the condition again. As long as the result of the condition tested is true, the program continues to carry out the same subtask. The first time the test is not true, the program proceeds onward.
Note in Figure 6.1 that whatever the task of Figure 6.1a, work starts with the arrow into the top of the "box" representing the task and finishes with the arrow out of the bottom of the box. There is no mention of what goes on inside the box.
In each of the three possible decompositions of Figure 6.1a (i.e., Figures 6.1b, lc, and Id), there is exactly one entrance into the construct and one exit out of the construct. Thus, it is easy to replace any task of the form of Figure 6.1a with whichever of its three decompositions apply. We will see how in the following example.
6.1.3 LC-3 Control Instructions to Implement the Three Constructs
Before we move on to an example, we illustrate in Figure 6.2 the use of LC-3 control instructions to direct the program counter to carry out each of the three decomposition constructs. That is, Figures 6.2b, 6.2c, and 6.2d correspond respectively to the three constructs shown in Figures 6.1b, 6.1c, and 6. Id.
We use the letters A, B, C, and D to represent addresses in memory containing LC-3 instructions. A, for example, is used in all three cases to represent the address of the first LC-3 instruction to be executed.
Figure 6.2b illustrates the control flow of the sequential decomposition. Note that no control instructions are needed since the PC is incremented from Address Bi to Address Bi + 1. The program continues to execute instructions through address Di. It does not return to the first subtask.
Figure 6.2c illustrates the control flow of the conditional decomposition.
First, a condition is generated, resulting in the setting of one of the condition codes. This condition is tested by the conditional branch instruction at Address B2. If the condition is true, the PC is set to Address C2+I, and subtask 1 is executed. (Note: x corresponds to the number of instructions in subtask 2.) If the condition is false, the PC (which had been incremented during the FETCH
158 chapter 6 Programming
phase of the branch instruction) fetches the instruction at Address B2-bl, and subtask 2 is executed. Subtask 2 terminates in a branch instruction that at Address C2 unconditionally branches to D2+ l . (Note: j corresponds to the number of instructions in subtask 1.)
Figure 6.2d illustrates the control flow of the iterative decomposition. As in the case of the conditional construct, first a condition is generated, a condition code is set, and a conditional branch is executed. In this case, the condition bits of the instruction at address B3 are set to cause a conditional branch if the condition generated is false. If the condition is false, the PC is set to address D3+ l . (Note:
z corresponds to the number of instructions in the subtask in Figure 6.2d.) On the other hand, as long as the condition is true, the PC will be incremented to B3+I, and the subtask will be executed. The subtask terminates in an unconditional branch instruction at address D3, which sets the PC to A to again generate and test the condition. (Note: w corresponds to the total number of instructions in the decomposition shown as Figure 6.2d.)
Now, we are ready to move on to an example.
6.1.4 The Character Count Example from Chapter 5, Revisited Recall the example of Section 5.5. The statement of the problem is as follows:
"We wish to count the number of occurrences of a character in a file. The character
(a) (b)
Start
!
Input a character. T h e n scan a file, counting occurrences of that character. Finally, display on the monitor the number of occurrences of that character (up to 9).
!
Stop
Initialize: Put initial values into all locations that will be needed to carry out this task.
* Input a character.
* Set up the pointer to the first location in the file that will be scanned.
* Get the first character from the file.
* Zero the register that holds the count.
\(
Scan the file, location by location, incrementing the counter if the character matches.
\ f
Display the count on the monitor.
V
Stop
F i g u r e 6 . 3 Stepwise refinement of the character count p r o g r a m
in question is to be input from the keyboard; the result is to be displayed on the monitor."
The systematic decomposition of this English language statement of the prob-lem to the final LC-3 impprob-lementation is shown in Figure 6.3. Figure 6.3a is a brief statement of the problem.
In order to solve the problem, it is always a good idea first to examine exactly what is being asked for, and what is available to help solve the problem. In this case, the statement of the problem says that we will get the character of interest from the keyboard, and that we must examine all the characters in a file and determine how many are identical to the character obtained from the keyboard.
Finally, we must output the result.
1 6 0 chapter 6 Programming
(c) (d)
F i g u r e 6 . 3 Stepwise refinement of the character count p r o g r a m (continued)
To do this, we will need a mechanism for scanning all the characters in a file, and we will need a counter so that when we find a match, we can increment that counter.
We will need places to hold all these pieces of information:
1. The character input from the keyboard.
2. Where we are (a pointer) in our scan of the file.
3. The character in the file that is currently being examined.
4. The count of the number of occurrences.
We will also need some mechanism for knowing when the file terminates.
The problem decomposes naturally (using the sequential construct) into three parts as shown in Figure 6.3b: (A) initialization, which includes keyboard input of the character to be "counted," (B) the actual process of determining how many
(e)
Start
R2 < - 0 (count)
I
RO < - input
R3 < - starting address
J
Display output
i
Stop
Figure 6.3 Stepwise refinement of the character count program (continued)
occurrences of the character are present in the file, and (C) displaying the count on the monitor.
We have seen the importance of proper initialization in several examples already. Before a computer program can get to the crux of the problem, it must have the correct initial values. These initial values do not just show up in the GPRs
184 chapter 6 Programming
by magic. They get there as a result of the first set of steps in every algorithm: the initialization of its variables.
In this particular algorithm, initialization (as we said in Chapter 5) consists of starting the counter at 0, setting the pointer to the address of the first character in the file to be examined, getting an input character from the keyboard, and getting the first character from the file. Collectively, these four steps comprise the initialization of the algorithm shown in Figure 6.3b as A.
Figure 6.3c decomposes B into an iteration construct, such that as long as there are characters in the file to examine, the loop iterates. B1 shows what gets accomplished in each iteration. The character is tested and the count incremented if there is a match. Then the next character is prepared for examination. Recall from Chapter 5 that there are two basic techniques for controlling the number of iterations of a loop: the sentinel method and the use of a counter. This program uses the sentinel method by terminating the file we are examining with an EOT (end of text) character. The test to see if there are more legitimate characters in the file is a test for the ASCII code for EOT.
Figure 6.3c also shows the initialization step in greater detail. Four LC-3 registers (R0, R l , R2, and R3) have been specified to handle the four requirements of the algorithm: the input character from the keyboard, the current character being tested, the counter, and the pointer to the next character to be tested.
Figure 6.3d decomposes both B1 and C using the sequential construct. In the case of B l , first the current character is tested (B2), and the counter incremented if we have a match, and then the next character is fetched (B3). In the case of C, first the count is prepared for display by converting it from a 2's complement integer to ASCII (CI), and then the actual character output is performed (C2).
Finally, Figure 6.3e completes the decomposition, replacing B2 with the elements of the condition construct and B3 with the sequential construct (first the pointer is incremented, and then the next character to be scanned is loaded).
The last step (and the easy part, actually) is to write the LC-3 code corre-sponding to each box in Figure 6.3e. Note that Figure 6.3e is essentially identical to Figure 5.7 of Chapter 5 (except now you know where it all came from!).
Before leaving this topic, it is worth pointing out that it is not always possible to understand everything at the outset. When you find that to be the case, it is not a signal simply to throw up your hands and quit. In such cases (which realistically are most cases), you should see if you can make sense of a piece of the problem, and expand from there. Problems are like puzzles; initially they can be opaque, but the more you work at it, the more they yield under your attack. Once you do understand what is given, what is being asked for, and how to proceed, you are ready to return to square one (Figure 6.3a) and restart the process of systematically decomposing the problem.
6.2 Debugging
Debugging a program is pretty much applied common sense. A simple example comes to mind: You are driving to a place you have never visited, and somewhere along the way you made a wrong turn. What do you do now? One common
"driving debugging" technique is to wander aimlessly, hoping to find your way back. When that does not work, and you are finally willing to listen to the person sitting next to you, you turn around and return to some "known" position on the route. Then, using a map (very difficult for some people), you follow the directions provided, periodically comparing where you are (from landmarks you see out the window) with where the map says you should be, until you reach your desired destination.
Debugging is somewhat like that. A logical error in a program can make you take a wrong turn. The simplest way to keep track of where you are as compared to where you want to be is to trace the program. This consists of keeping track of the sequence of instructions that have been executed and the results produced by each instruction executed. When you examine the sequence of instructions executed, you can detect errors in the control flow of the program. When you compare what each instruction has done to what it is supposed to do, you can detect logical errors in the program. In short, when the behavior of the program as it is executing is different from what it should be doing, you know there is a bug.
A useful technique is to partition the program into parts, often referred to as modules, and examine the results that have been computed at the end of execu-tion of each module. In fact, the structured programming approach discussed in Section 6.1 can help you determine where in the program's execution you should examine results. This allows you to systematically get to the point where you are focusing your attention on the instruction or instructions that are causing the problem.
6.2.1 Debugging Operations
Many sophisticated debugging tools are offered in the marketplace, and undoubt-edly you will use many of them in the years ahead. In Chapter 15, we will examine some debugging techniques available through dbx, the source-level debugger for the programming language C. Right now, however, we wish to stay at the level of the machine architecture, and so we will see what we can accomplish with a few very elementary interactive debugging operations. When debugging interactively, the user sits in front of the keyboard and monitor and issues commands to the computer. In our case, this means operating an LC-3 simulator, using the menu available with the simulator.
It is important to be able to
1. Deposit values in memory and in registers.
2. Execute instruction sequences in a program.
3. Stop execution when desired.
4. Examine what is in memory and registers at any point in the program.
These few simple operations will go a long way toward debugging programs.
Set Values
It is useful to deposit values in memory and in registers in order to test the execution of a part of a program in isolation, without having to worry about parts
164 chapter 6 Programming
of the program that come before it. For example, suppose one module in your program supplies input from a keyboard, and a subsequent module operates on that input. Suppose you want to test the second module before you have finished debugging the first module. If you know that the keyboard input module ends up with an ASCII code in RO, you can test the module that operates on that input by first placing an ASCII code in RO.
Execute Sequences
It is important to be able to execute a sequence of instructions and then stop execu-tion in order to examine the values that the program has computed. Three simple mechanisms are usually available for doing this: run, step, and set breakpoints.
The R u n command causes the program to execute until something makes it stop. This can be either a HALT instruction or a breakpoint.
The Step command causes the program to execute a fixed number of instruc-tions and then stop. The interactive user enters the number of instrucinstruc-tions he/she wishes the simulator to execute before it stops. When that number is 1, the com-puter executes one instruction, then stops. Executing one instruction and then stopping is called single-stepping. It allows the person debugging the program to examine the individual results of every instruction executed.
The Set Breakpoint command causes the program to stop execution at a specific instruction in a program. Executing the debugging command Set Break-point consists of adding an address to a list maintained by the simulator. During the FETCH phase of each instruction, the simulator compares the PC with the addresses in that list. If there is a match, execution stops. Thus, the effect of setting a breakpoint is to allow execution to proceed until the PC contains the address of the breakpoint. This is useful if one wishes to know what has been computed up to a particular point in the program. One sets a breakpoint at that address in the program and executes the Run command. The program executes until that point, thereby allowing the user to examine what has been computed up to that point.
(When one no longer wishes to have the program stop execution at that point, one can remove the breakpoint by executing the Clear Breakpoint command.)
Display Values
Finally, it is useful to examine the results of execution when the simulator has stopped execution. The Display command allows the user to examine the contents
Finally, it is useful to examine the results of execution when the simulator has stopped execution. The Display command allows the user to examine the contents