BasicOS
Basics
of
Operating
Systems

General information

Processes and memory management

You don't need to submit source files or reports for this session.

You will need to log into a PC running Linux. If you are at Eurecom, simply log on a PC of rooms 52 or 53. If you were to use another PC running GNU/Linux, I cannot guarantee that the lab works in the same way (outputs could be different, manual pages could be different, etc.).

The session is dedicated to the study of processes and their memory in Operating Systems. It uses C pointers so, if you don't feel at ease with them, you may first read this page on C pointers, until section 4 included. You may also only read the two first sections, and come back to this page whenever necessary.

I. Basics

  1. Open the slides on Processes
  2. Without reading the slides, are you able to list the three default streams of a Linux process?
  3. Reproduce all commands given in the lecture. When a cmd is given, imagine a concrete command that could be used instead of cmd. For each cmd of the slides find at least one concrete command that works and one that fails.
  4. Write a short C program that causes an error, and thats prints to stderr a text corresponding to this error. Compile and run your program with a redirection of the error stream to a file. Check that the file contains the expected error message.

II. Memory of processes

This exercise studies the relation between the memory area for global variables, the heap, and the stack.
  1. Create a file named proc1.c with the following code:
    int * pointerToRandomValue;
    
    int computeRandom( int maxValue ) {
      int myRand = rand() % maxValue;
      pointerToRandomValue = &myRand;
      return myRand;
    }
    
    int main( int argc, char *argv[] ) {
      srand(time(NULL)); /* Seed initialization */
    
      int returned = computeRandom(20);
    
      //printf("Hello world!\n");
    
      printf("The random value via the pointer is: %d\n", *pointerToRandomValue);
      printf("The returned random value is: %d\n", returned)
      
      return 0;
    }
  2. Compile:
    $ gcc -Wall -o proc1 proc1.c
    
    The compiler should issue warnings and errors. Fix them and execute the program. Note the result you get.
    (Help me!) There is a semicolon missing, as well as header files to include. You can find out which header files must be included by using man on the external function calls.

  3. Copy proc1.c to proc2.c. In proc2.c uncomment the line with the call to printf. Compile and execute. What happens? Try to explain.
    (Help me!) The pointer points somewhere in the memory region that the call to computeRandom allocated for its stack frame. Immediately after returning from computeRandom this memory region still contains the random value. Yet, if another function (printf) is called before the random value is read, the same memory region is used by the new function call and the random value is overwritten.

  4. To investigate this situation we will use gdb, the command-line debugger, that allows examining the content of the memory of a program during execution, and even modifying it. Note: depending on the gdb version the output can differ from the provided one.
    1. Recompile proc2.c with the -g option to generate the debugging symbols:
      $ gcc -Wall -g -o proc2 proc2.c
    2. Execute with gdb (to quit type quit, to restart from the beginning type jump _start):
      $ gdb proc2
    3. Display the source code and find the line number of the call to computeRandom:
      (gdb) list
      1	#include <stdio.h>
      2	#include <stdlib.h>
      3	#include <time.h>
      4	
      5	int * pointerToRandomValue;
      6	
      7	int computeRandom( int maxValue ) {
      8	  int myRand = rand() % maxValue;
      9	  pointerToRandomValue = &myRand;
      10	  return myRand;
      (gdb) list
      11	}
      12	
      13	int main( int argc, char*argv[] ) {
      14	  srand(time(NULL)); /* Seed initialization */
      15	
      16	  int returned = computeRandom(20);
      17	
      18	  printf("Hello world!\n");
      19	
      20	  printf("The random value via the pointer is: %d\n", *pointerToRandomValue);
      Note: you can also use the list command with a line number to display the source code around a given line, e.g. line 9:
      (gdb) list 9
      4	
      5	int * pointerToRandomValue;
      6	
      7	int computeRandom( int maxValue ) {
      8	  int myRand = rand() % maxValue;
      9	  pointerToRandomValue = &myRand;
      10	  return myRand;
      11	}
      12	
      13	int main( int argc, char*argv[] ) {
      Note: to know more about the gdb command CMD simply type help CMD. Note: commands can be abbreviated if unambiguous (e.g., l for list, b for break...) In our example the call is at line 16 (but it could be different in your case, check carefully). Put a breakpoint on the line:
      (gdb) break 16
      Breakpoint 1 at 0x125d: file proc2.c, line 16.
    4. Run the program from start (if offered to Enable debuginfod for this session answer yes):
      (gdb) run
      Starting program: /homes/pacalet/tmp/proc2 
      [Thread debugging using libthread_db enabled]                                                                                                                                  
      Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
      
      Breakpoint 1, main (argc=1, argv=0x7fffffffe3c8) at proc2.c:16
      16	  int returned = computeRandom(20);
    5. The program is stopped on the breakpoint. Examine variable pointerToRandomValue:
      (gdb) print pointerToRandomValue
      $1 = (int *) 0x0
      Try to read the memory location it points to:
      (gdb) print *pointerToRandomValue
      Cannot access memory at address 0x0
      Address 0x0 is not accessible ("null" address).

    6. Continue the program step by step:
      (gdb) step
      computeRandom (maxValue=32767) at proc2.c:7
      7    int computeRandom( int maxValue ) {
      After this first step the execution entered the computeRandom function. Display the list of called functions, that is, the backtrace of all stack frames:
      (gdb) bt
      #0  computeRandom (maxValue=20) at proc2.c:7
      #1  0x0000555555555267 in main (argc=1, argv=0x7fffffffe3c8) at proc2.c:16
      As expected function main was first called and then function computeRandom.
    7. Our goal is to debug what happens around the call to the rand function. Display the source code (list), find the call to rand and add a breakpoint after the call (check your own line numbers):
      (gdb) list
      2	#include <stdlib.h>
      3	#include <time.h>
      4	
      5	int * pointerToRandomValue;
      6	
      7	int computeRandom( int maxValue ) {
      8	  int myRand = rand() % maxValue;
      9	  pointerToRandomValue = &myRand;
      10	  return myRand;
      11	}
      (gdb) break 9
      Breakpoint 2 at 0x555555555215: file proc2.c, line 9.
      Continue the execution until the breakpoint:
      (gdb) continue 
      Continuing.
      
      Breakpoint 6, computeRandom (maxValue=20) at proc2.c:9
      9	  pointerToRandomValue = &myRand;
      Variable pointerToRandomValue should still contain address 0 and myRand should contain a random value between 0 and 20; check that:
      (gdb) print pointerToRandomValue
      $2 = (int *) 0x0
      (gdb) print myRand
      $3 = 11
      Go one step ahead and check again the value of the 2 variables:
      (gdb) step
      ...
      (gdb) print pointerToRandomValue
      $4 = (int *) 0x7fffffffe274
      (gdb) print *pointerToRandomValue
      $5 = 11
      (gdb) print &myRand
      $6 = (int *) 0x7fffffffe274
      (gdb) print &maxValue
      $7 = (int *) 0x7fffffffe26c
      We can now print a partial view of the stack. Note: x/8dw is the gdb command that examines the memory (x), and print 8 words (8w), in decimal (d) format. Adapt the address, use that of maxValue (0x7fffffffe26c in our example):
      (gdb) x/8dw 0x7fffffffe26c
      0x7fffffffe26c:	20	0	11	926993152
      0x7fffffffe27c:	-49932742	-7504	32767	1431655015
    8. In function main add a breakpoint on the printf("Hello world\n") and another on the following call to printf:
      (gdb) break 18
      Breakpoint 3 at 0x55555555526a: file proc2.c, line 18.
      (gdb) break 20
      Breakpoint 4 at 0x555555555279: file proc2.c, line 20.
    9. We can now continue the execution, exit the computeRandom function, and stop just before the first printf:
      (gdb) continue
      Continuing.
      
      Breakpoint 3, main (argc=1, argv=0x7fffffffe3c8) at proc2.c:18
      18	  printf("Hello world!\n");
    10. Check if the content of the stack frame of the computeRandom call is still in memory:
      (gdb) x/8dw 0x7fffffffe26c
      0x7fffffffe26c:    20    -5994    11    474799616
      0x7fffffffe27c:    -473136351    -5968    32767    1431655015
      Even after the program has left the computeRandom function, its stack frame is still available. Continue to call the first following function, that is, printf("Hello world\n"), and stop before the second printf:
      (gdb) continue
      Continuing.
      Hello world!
      
      Breakpoint 4, main (argc=1, argv=0x7fffffffe3c8) at proc2.c:20
      20	  printf("The random value via the pointer is: %d\n", *pointerToRandomValue);
    11. Print again the content of the stack frame of the computeRandom call:
      (gdb) x/8dw 0x7fffffffe26c
      0x7fffffffe26c:	32767	0	0	-7208
      0x7fffffffe27c:	32767	1431666072	21845	1431655033
      As we can see, the content has been overwritten by the printf("Hello world\n") call.
    Conclusion: never use values stored in a stack frame after the function that allocated the stack frame has returned, because they can be modified anytime by other function calls.

  5. As we cannot reliably pass the generated random value through the stack frame from the computeRandom call to the main, we must find another way. Copy proc2.c to proc3.c. Edit proc3.c and, without modifying the definition of pointerToRandomValue nor the code of main, try to achieve the expected result. You will probably need to allocate some memory on the heap with malloc or calloc. Implement and test with gdb. Do not forget to test the return value of malloc.

  6. BONUS work #1. Code two new programs, procSender.c and procReceiver.c, with just one main function in each. In procSender.c the main function shall generate a printable random string of characters, and print it to its standard output stream and to its standard error stream. Do not use static arrays, use dynamic allocations. Do not forget to test the return value of malloc and to free the allocated memory after use. In procReceiver.c the main function shall read all strings from its standard input stream, and print them to its standard output stream. Compile the two programs and execute them in a pipe:
    $ procSender | procReceiver

  7. BONUS work #2. Modify procReceiver.c such that it automatically indents a C source code. Assume:
    • The source code contains no curly braces in comments or literal text strings (e.g., char *cb = "}{";).
    • All curly braces are alone in a separate line.
    • Each opening curly brace ({) increases the indentation level by 4 spaces for the lines that follow.
    • Each closing curly brace (}) decreases it by 4 spaces for its own line and the lines that follow.
    • Finally, all curly braces are properly balanced and nested.
    Compile and test procReceiver on its own source code:
    $ cat procReceiver.c | procReceiver