Step through the Crash

Now that we understand why the do-nothing program crashes, we will use GDB to step through the execution of the do-nothing test in PintOS, starting from when the kernel boots. Our goal is to find out how we can modify the PintOS user program loader so that do-nothing does not crash, while becoming acquainted with how PintOS supports user programs. To do this, select the Debug do-nothing from the top of the VSCode debugging pane.

As a side note, we have added two files .vscode/launch.json and .vscode/tasks.json to the starter repository that contain configurations enabling to debug the PintOS kernel from inside VSCode. Here is an excerpt from the file .vscode/tasks.json:

{
  "version": "2.0.0",
  "tasks": [
    {
        "label": "[P1] compile",
        "type": "shell",
        "command": "make",
        "options": {
            "cwd": "${workspaceFolder}/src/userprog"
        }
    },
    {
        "label": "[P1] Run Test in GDB mode",
        "type": "shell",
        "isBackground": true,
        "problemMatcher": [],
        "dependsOn": [
            "[P1] compile"
        ],
        "command": "pintos -v -k -T 60 --bochs --gdb  -- -q  run do-nothing",
        "options": {
            "cwd": "${workspaceFolder}/src/userprog/build"
        }
    }
  ]
}

Please familiarize yourself with those files so you can modify them as needed in the future. For more information about this file format, please see the corresponding documentation.

GDB should now be open and your program should stop executing at the entry point to PintOS. At a high level, the following must happen before PintOS can start the do-nothing process:

  • The BIOS reads the PintOS boot-loader (src/threads/loader.S) from the first sector of the disk into memory at address 0x7c00.
  • The boot-loader reads the kernel code from disk into memory at address 0x20000 and then jumps to the kernel entrypoint (src/threads/start.S).
  • The code at the kernel entrypoint switches to 32-bit protected mode 1 and then calls main (src/threads/init.c).
  • The main function boots PintOS by initializing the scheduler, memory subsystem, interrupt vector, hardware devices, and file system.

You’re welcome to read the code to learn more about this setup, but you don’t need to understand how this works for the PintOS projects or for this class.

Set a breakpoint at run_task and continue in GDB to skip the setup. As you can see in the code for run_task, PintOS executes the do-nothing program (specified on the PintOS command line), by invoking

process_wait(process_execute("do-nothing"));

from run_task. Both process_wait and process_execute are in src/userprog/process.c.

If you use the gdb commands listed below from the debug window in VSCode you need to prefix them with -exec, i.e., instead of typing info registers in that window you need to use -exec info registers to pass on the command to gdb.

Now, answer the following questions in results/answers.md:

Q1: Step into the process_execute function. What is the name and address of the PintOS thread running this function? What other threads are present in PintOS at this time? Copy the content of their struct thread data structures to answers.md. (Hint: for the last part, dumplist &all_list thread allelem may be useful. The command p running_thread() may also be of help)

Q2: What is the backtrace for the current PintOS thread? Copy the backtrace from GDB as your answer and also copy down the line of C code corresponding to each function call.

Q3: Set a breakpoint at start_process and continue to that point. What is the name and address of the PintOS thread running this function? What other threads are present in PintOS at this time? Copy the content of their struct thread data structures to answers.md.

Q4: Where is the thread running start_process created? Copy down this line of code.

Q5: Step through the start_process function until you have stepped over the call to load. Note that load sets the eip and esp fields in the if_ structure. Print out the value of all members of the if_ structure, displaying the values in hex (hint: print/x if_). Alternatively you can select the Variables/Locals pane in the Debug pane to see the values. Add the content of if_ to your answers.md

Q6: The first instruction in the asm volatile statement sets the stack pointer to the bottom of the if_ structure. The second one jumps to intr_exit. The comments in the code explain what’s happening here. Step into the asm volatile statement, and then step through the instructions. As you step through the iret instruction, observe that the function “returns” into userspace. Why does the processor switch modes when executing this function? Feel free to explain this in terms of the values in memory and/or registers at the time iret is executed, and the functionality of the iret instruction.

Q7: Once you’ve executed iret, type info registers to print out the contents of registers. Include the output of this command in answers.md. How do these values compare to those when you printed out if_?

Q8: Notice that if you try to get your current location with backtrace you’ll only get a hex address. This is because because the debugger only loads in the symbols from the kernel. Now that we are in userspace, we have to load in the symbols from the PintOS executable we are running, namely do-nothing. To do this, use loadusersymbols tests/userprog/do-nothing. Now, using backtrace, you’ll see that you’re currently in the _start function. Using the disassemble and stepi commands (or step-over in the disassembler window), step through userspace instruction by instruction until the page fault occurs. At this point, the processor has immediately entered kernel mode to handle the page fault, so backtrace will show the current stack in kernel mode, not the user stack at the time of the page fault. However, you can use btpagefault to find the user stack at the time of the page fault. Copy the output of btpagefault and add it to your answers.md.

The next step for this assignment is described here.