BasicOS
Basics
of
Operating
Systems

General information


Processes and memory management

This session is dedicated to the study of processes and their memory in Operating Systems.

You don't need to submit source files or reports for this session. 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.).



I. Basics

  1. Open the slides Processes

  2. Without reading the slides, are you able to cite 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". Find at least, for each "cmd" of the slides, one concrete command that works and one that fails.

  4. Write a short C program that generates an error, and thats prints to stderr a text corresponding to this error. Compile, start your program with a redirection of error stream to a file. Do you see the expected error message in this file once the program has executed?



II. Memory of processes


This exercise studies the relation between the memory area for global variables, the heap, and the stack. This lab 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..
  1. Create a file with the following code (name it: proc1.c):

  2. int * randomValue;
    
    int computeRandom( int maxValue ) {
      int myRand = rand() % maxValue;
      randomValue = &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", *randomValue);
      printf("The returned random value is: %d\n", returned)
      
    }
    

  3. Compile this file to an executable named "proc1".

  4. $ gcc -Wall -o proc1 proc1.c
    

    The compiler should issue warnings and errors. Correct them, and execute the program. Remember the result you get.
    (Help me!) There is a semicolon missing, as well as external h files to include. You can find out which files should be included by using "man" on the external function calls.


  5. Copy proc1.c to proc2.c. Now, work with proc2.c. Uncomment the line with the call to printf. Compile, and execute. What happens? Clearly explain the problem.
  6. (Help me!) The pointer refers to a element on the stack. After returning from computeRandom(), the memory on the stack used for storing the call to computeRandom still contains the random value. Yet, if another function is called before the random value is used, then the stack is erased by the other function call.


  7. Let us now use a debugguer to better investigate this situation. A well-known command-line debugger is gdb. gdb can be used to investigate the content of the memory of a program at execution, and manipulate this memory if necessary. Beware: you may get an output different from the one I provide, depending on the gdb version you use.

    1. Now, let us continue to work again with proc2.c. First, you need to recompile proc2.c with the debug symbols:
    2. $ gcc -Wall -g -o proc2 proc2.c
      

    3. Now, execute the executable with gdb as follows:
    4. $ gdb proc2
      

      You can quit gdb by typing quit. Also, to restart from the beginning of the code, you can type :
      (gdb) jump _start
      

      Don't do it now, we still need to investigate the memory of your program.

    5. Put a break point before calling computeRandom(). For this, first list the lines of your program, and select the correct line:
    6. (gdb) list  
      1	...
      2	...
      3	...
      4	
      5	int * randomValue;
      6	
      7	int computeRandom( int maxValue ) {
      8	  int myRand = rand() % maxValue;
      9	  randomValue = &myRand;
      10	  return myRand;
      (gdb) list
      11	}
      12	
      13	
      14	int main( int argc, char*argv[] ) {
      15	  srand(time(NULL)); /* Seed initialization */
      16	
      17	  int returned = computeRandom(20);
      18	
      19	  printf("Hello world\n");
      20	  
      (gdb) list
      21	  printf("The random value via the pointer is: %d\n", *randomValue);
      22	  printf("The returned random value is: %d\n", returned);
      23	  
      24	}
      
      

      Note: you can also list the code around a given line, e.g. line 9, by typing:
      (gdb) list 9
      4	
      5	int * randomValue;
      6	
      7	int computeRandom( int maxValue ) {
      8	  int myRand = rand() % maxValue;
      9	  randomValue = &myRand;
      10	  return myRand;
      11	}
      12	
      13	
      

      We now know that the breakpoint should be at line 17 (but do check on your code at what line the breakpoint must be set):
      (gdb) break 17
      Breakpoint 1 at 0x125d: file proc2.c, line 17.
      

    7. You can now run the program:
    8. (gdb) run
      Starting program: /homes/standard/tmp/proc2 
      
      Breakpoint 1, main (argc=1, argv=0x7fffffffe9a8) at proc2.c:17
      17	  int returned = computeRandom(20);
      

    9. The program is stopped, let us investigate the value of variable randomValue:
    10. (gdb) print randomValue
      $1 = (int *) 0x0
      

      Thus, randomValue points to address 0, also called null. Obviously, the value at address 0x0 is not accessible:
      (gdb) print *randomValue
      Cannot access memory at address 0x0
      

    11. Let us now execute the program one line of code at a time (i.e., we do a step-by-step execution):
    12. (gdb) step
      computeRandom (maxValue=32767) at proc2.c:7
      7	int computeRandom( int maxValue ) {
      
      After a first step, the program is now executoing the computeRandom function. Let us see the list of called functions (e.g., an excerpt of the stack):
        (gdb) bt
      #0  computeRandom (maxValue=32767) at proc2.c:7
      #1  0x0000555555555267 in main (argc=1, argv=0x7fffffffe9a8) at proc2.c:17
      
      As expected, first, function main was called, and now the program is currently executing function computeRandom.

    13. Let us now add a breakpoint to the line after the call to rand() (our objective is now to debug rand()):
    14. (gdb) break 9
      Breakpoint 2 at 0x555555555215: file proc2.c, line 9.
      
      We can now go to the next breakpoint:
      (gdb) continue
      Continuing.
      
      Breakpoint 2, computeRandom (maxValue=20) at proc2.c:9
      9	  randomValue = &myRand;
      
      Let's check the value of variable: randomValue still points to the null address, and myRand should contain a random value between 0 and 20:
      (gdb) print randomValue
      $2 = (int *) 0x0
      (gdb) print myRand
      $3 = 17
      
      Let us now go one step ahead, and let us check again to the value of both variables:
      (gdb) step
        ...
      (gdb) print randomValue
      $4 = (int *) 0x7fffffffe874
      (gdb) print *randomValue
      $5 = 17
      p &myRand
      $6 = (int *) 0x7fffffffe874
      (gdb) p &maxValue
      $7 = (int *) 0x7fffffffe86c
      
      We can now print a partial view of the stack. "x" means memory, 8 for 8 words ("w"), "d" is for decimal:
      (gdb) x/8dw 0x7fffffffe86c
      0x7fffffffe86c:	20	-5994	17	474799616
      0x7fffffffe87c:	-473136351	-5968	32767	1431655015
      

    15. Let us now exit the function call and go to the next instruction after the function call (i.e., the printf("Hello world\n")), and let us also add a breakpoint to the printf after:
    16. (gdb) break 19
      Breakpoint 3 at 0x55555555526a: file proc2.c, line 19.
      (gdb) break 21
      Breakpoint 4 at 0x555555555276: file proc2.c, line 21.
      

      We can now go to before the first printf, and so, we exit the computeRandom function:
      (gdb) continue
      Continuing.
      
      Breakpoint 3, main (argc=1, argv=0x7fffffffe9a8) at proc2.c:19
      19	  printf("Hello world\n");
      

      Let's see if the previous values used in computeRandom are still in memory:
      (gdb) x/8dw 0x7fffffffe86c
      0x7fffffffe86c:	20	-5994	17	474799616
      0x7fffffffe87c:	-473136351	-5968	32767	1431655015
      
      Stack data are still unchanged even after the program has left the computeRandom function. If we call the first function, e.g. printf(), then we will see that the stack has been modified by this function call:
      (gdb) continue
      Continuing.
      Hello world
      
      Breakpoint 4, main (argc=1, argv=0x7fffffffe9a8) at proc2.c:21
      21	  printf("The random value via the pointer is: %d\n", *randomValue);
      

      Now, let us print the content of the stack:
      (gdb) x/8dw 0x7fffffffe86c
      0x7fffffffe86c:	32767	1431654656	21845	-5728
      0x7fffffffe87c:	32767	0	0	1431655030
      

    As a conclusion, we should never use values allocated in the stack after the related function has returned, because values on the stack might be modified whenever another function is called.

  8. Now, we would like to fix this issue without modifying both calls to printf nor modifying the definition of randomValue. For this, probably you will need to allocate memory on the heap with malloc. Implement, and test with gdb. Do not forget to test the return value of malloc and to use free to disallocate memory after use. Last but not least, use another file to do this (proc3.c).

  9. Generate a random string, and print it. The random string should be generated in a function called computeRandomString() and the random string shall be printed by main().
  10. As done in the program generating a random number, use two ways to return the random String from computeRandomString() to main.

  11. BONUS work. Modify your previous program (let us call it procSender) to simply send the random string to stdout. Then, make a program (let us call it procReceiver) than reads a String from stdin and prints it to stdout. Once the two programs have been successfully compiled, you should be able to execute something like:
  12. $ procSender > procReceiver
    

  13. BONUS work. Now, let us assume that procReceiver can automatically indent a C program. If you were to do something like:
  14. $ cat toto.c > procReceiver
    
    then procReceiver would print toto.c after indentation, assuming toto.c is not correctly indented. Program this and test.