Infrastructure: Simple Debugger

Infrastructure - Improving Efficiency in Project Development

In PA, infrastructure refers to the tools and instruments that support the development of a project. In principle, infrastructure is not part of textbook knowledge, but as a project of a certain size, the quality of the infrastructure can affect the progress of the project, and even determine the success or failure of the project, which you can not experience in the programming class.

In fact, you've already experienced what infrastructure can do for you. Our framework code already provides a Makefile for one-click compilation of NEMU. Suppose we don't provide one-click compilation, and you have to manually type in the gcc command to compile the source files: Suppose it takes you 10 seconds to manually type in a gcc command (you have to type in a lot of compilation options, and 10 seconds is very fast), and there are 30 source files under the NEMU project, how much time would you need to spend in order to compile the NEMU executable? In order to compile the NEMU executable, how much time do you need to spend? However, you also need to develop NEMU with a lot of recompiling, let's say you need to compile NEMU 500 times to complete the PA, with in a semester, how much time you just spend on typing compilation commands?

Some projects take longer to build even with tools. For example, the vivado/quartus IDE can take anywhere from half an hour to an hour to generate a bit stream, which means that after you have written the code, you may have to wait up to an hour before you can verify that your code is correct. This is because the process is not as simple as compiling a program, and there are many algorithmic NPC issues to deal with. In order to produce a decent bit stream, the hardware development tools cost more than gcc to solve these NPC problems. This is where infrastructure becomes even more important, and having tools that can help you verify multiple aspects at once can save you countless "hours".

Google's internal development team places a high value on infrastructure, calling tools that benefit one project an Adder, and tools that benefit multiple projects a Multiplier. As the name implies, these tools can multiply the efficiency of a project's development. In academia, the goal of many research efforts is also to improve development efficiency, such as automated bug detection and fixing, automated verification, and easy-to-develop programming models. In PA, the infrastructure also manifests itself in different ways, and we will discuss other aspects in the future.

You will certainly be involved in larger projects than PA in the future, and the question of how to improve the efficiency of project development is also an important one. Hopefully, in the course of completing PA, you will gain a new understanding of infrastructure: where there's code, there's infrastructure. As you gain knowledge, you may be able to contribute to these uncharted territories and make a difference to developers all over the world.

True Stories

In yzh's group, there was an incident where the quality of the research was not satisfactory before the submission deadline due to poor infrastructure.

The work required running different tests to verify the results, and it took about 24 hours to run all the tests on two 4-core, 8-thread PCs. Each time a change was made to the design, all the tests had to be re-run, so it took 24 hours to get the results after changing one line of code. In fact, three months before the submission deadline, we learned that a 112-core server was available. Deploying the test environment on this server was expected to reduce the total test time to 1/5 of the original time (1/5 is a comprehensive figure given that multi-core servers has half of the CPU frequency compares to PCs, and the architecture is two generations behind).

But the student leading the project didn't realize the importance of the infrastructure: he was always testing on a PC. In fact, reducing the total testing time to 1/5th of the original time meant that the chances of improving the design were five times greater. As a result, by the deadline, the design was still being revised, the tests were still being run over and over again, and he had no choice but to submit the paper with a version of the design that needed to be improved.

The Simple Debugger (sdb) is a very important infrastructure in NEMU. We know that NEMU is a program that is used to execute other client programs, which means that NEMU has all the information about the execution of the client program at all times. However, this information is not easily accessible to outside debuggers (e.g., GDB). For example, when debugging NEMU through GDB, you will have a hard time setting breakpoints in a client program running in NEMU, but for NEMU, this is a less difficult task.

In order to improve the efficiency of debugging, but also as an exercise to familiarize with the framework code, we need to implement a simple debugger with the following functions in monitor (the relevant part of the code in the nemu/src/monitor/sdb/ directory), if you are not clear about the format and function of the commands, please refer to the following table:

CommandFormatExampleExplanation
Help(1)helphelpPrints help information for the command
Continue running(1)ccResume running the suspended program
Quit(1)qqExit NEMU
Single-step executionsi [N]si 10Lets the program pause after executing N instructions using single step execution,
When N is not given, the default is 1
Print program statusinfo SUBCMDinfo r
info w
Print register status
Print watchpoint information
Scan memory(2)x N EXPRx 10 $espFinds the value of the expression EXPR, uses the result as the starting memory
address, and outputs consecutive N 4 bytes in hexadecimal.
Expression evaluationp EXPRp $eax + 1Find the value of the expression EXPR, for EXPR supported
operations
See the chapter Expression evaluation in debugging.
Set watchpointw EXPRw *0x2000Suspend program execution when the value of expression EXPR changes.
Deleting a watchpointd Nd 2Deletes the watchpoint with ID N.

Remarks:

  • (1) The command has been implemented already.
  • (2) Compared to GDB, we have simplified it here by changing the format of the command.

Bugs that will find their way to you one day

You will need to use these features in future PAs to help you debug NEMU. If your implementation is faulty, you may end up with the following scenario: you implement a new feature, test it, scan a section of memory, and find that the output is not what you expected. You think there's something wrong with the new feature you just implemented, so you debug it. After days and nights of debugging, you realize with tears in your eyes that the memory scanning function is buggy!

If you want to avoid this kind of misery, you need to fully test a feature after you implement it. Over time, the cost of discovering the same bug becomes more and more expensive.

Parsing Commands

To make the simple debugger easy to use, NEMU interacts with the user through the readline library, which uses the readline() function to read commands from the keyboard. In contrast to gets(), readline() provides "line editing" functionality, most commonly used to scroll through the history using the up and down arrow keys. In fact, shell programs read in commands using readline(). For information on the function and return value of readline(), see

man readline

After reading a command from the keyboard, NEMU needs to parse the command and then perform the relevant action. The purpose of parsing a command is to identify the arguments in the command, for example, identifying si and 10 in a si 10 command, thus knowing that this is a command that executes 10 instructions in a single step. The parsing of commands is accomplished through a series of string processing functions, such as strtok() in the framework code. strtok() is a standard library function in C. If you have never used strtok() before and plan to continue parsing commands using strtok() in the framework code, be sure to check out

man strtok

Also, the cmd_help() function gives an example of using strtok(). In fact, there are many string manipulation functions, type the following.

man 3 str<TAB><TAB>

where <TAB> represents the TAB key on the keyboard. You'll see a lot of functions that start with str, among which you'll be familiar with strlen(), strcpy(), and so on. It's a good idea to look at the manual page for each of these string manipulation functions to see what they do, because you'll probably need some of them to help you parse commands. You can also write your own string function to parse commands.

How do I test a string manipulation function?

You may not be able to resist the urge to code: instead of RTFM, you can write your own. If that's the case, consider this: how would you test your own string handler?

If you're willing to RTFM, think about that, too, because you'll run into similar problems in PA2.

Another recommended string manipulation function is sscanf(), which is very similar to scanf(), except that sscanf() reads formatted content from a string, which is sometimes convenient for string parsing. If you've never used them before, RTFM, or STFW.

Single-step execution

Single-step execution is very simple, and the framework code already gives you a function that simulates CPU execution, so you just call it with the appropriate arguments. If you still don't know how to do this, RTFSC.

Printing Registers

Printing registers is even simpler. But since the structure of the registers is ISA-dependent, we wanted to abstract the ISA differences for the simple debugger. The framework code has prepared the following API for you:

// nemu/src/isa/$ISA/reg.c
void isa_reg_display(void);

After executing info r, call isa_reg_display(), which outputs all the registers directly via printf(). If you've never used printf() before, RTFM or STFW. If you don't know what to output, you can refer to the output in GDB.

Scanning Memory

Scanning memory isn't too difficult to implement. After parsing the command, you first evaluate the expression. But you haven't implemented expression evaluation yet, so for now you can implement a simple version: specify that the expression EXPR can only be a hexadecimal number, for example

x 10 0x80000000

This simplification allows you to avoid getting bogged down in the details of expression evaluation for a while. Once you have resolved the starting address of the memory to be scanned, you can use a loop to print out the specified length of memory data in hexadecimal. If you don't know how to do this, again, you can refer to the output in GDB. The question is, how do we access the client computer's memory? (The answer is already there.)

Once you have implemented the scanning of memory, you can print the memory around 0x80000000 or 0x100000, and you should see the code of the program, compare it with the contents of the built-in client program, and check if you have implemented it correctly.

implements single-step execution, print registers, and scan memory.

Once you are familiar with NEMU's framework, these functions are easy to implement, and we don't have any hard rules about the output format, so consider this as an exercise to familiarize yourself with GNU/Linux programming.

NEMU prints out single-step instructions by default (there are some traps in this, you'll need to RTFSC to see where the instructions are printed), so you can verify that single-step execution works.

Don't know how to do it? Well, it looks like you need to read through the RTFSC chapter again. If you've forgotten some of the notes, you should re-read them.

I'm afraid the code is wrong, what should I do?

Michael Stonebrakeropenopen in new window winner of the 2014 Turing Award, mentioned in an interview that he spent five years developing the world's first relational database system, Ingres. Ingres, 90% of which was spent getting it up and running. In other words, 90% of the time during the development process, the system was not working, was buggy, and needed to be debugged.

So, let's accept it: bugs are normal, and you need to wake up from the illusion of "code that compiles and runs in a single run" that you felt in your programming experiments back in the day. The important thing is that we need to use the right methods and tools to help us test and debug, and ultimately get the program to run. An example of this is the version control tool git, which can track changes in your code to find out when bugs were introduced, and can roll back to the last working version of your program if necessary.

In short, only by mastering the right methods and tools, can the fear of bugs be truly dispelled.

Tips

This is the end of PA1 Phase 1.