Q) Why do we need a virtual machine? Why can't we just work on the thoth machine?
A) You need root privileges to load/unload device driver modules to the kernel since device drivers execute in kernel space and someone can run malicious code inside a device driver to compromise the kernel. We cannot give out root privileges to every student for security. Also, even if a student has no malicious intent, she can easily crash the kernel by writing a buggy device driver, and that would result in the thoth machine being unavailable to all students.
Q) Why are we working and compiling in thoth? Can't we compile on the virtual machine?
A) Yes, you can work and compile inside the Debian Linux virtual machine. It has GCC installed as well as the Make utility. You may be tempted to do so as it can be a pain to SCP binaries back and forth. But do so at your own peril. If your device driver malfunctions and corrupts the kernel, that may force you to reboot the virtual machine. That may cause you to lose files, even after saving them. The whole OS may refuse to even boot after a corruption. If you choose to do this, make sure you backup your source code outside your virtual machine periodically (e.g. using SCP). I will not accept excuses pertaining to not backing up your files.
Q) What do I do if scp (or anything else that used to work) suddenly does not work on the virtual machine?
A) Follow these instructions:
Try downloading the file using a program like WinSCP on to your local machine. This is to test if something is wrong with your laptop or your DNS server.
If the above is successful, try rebooting your QEMU virtual machine (by typing 'poweroff' on the command line, and then starting up the machine again.) Then try scp again.
If the above fails, try doing 'vagrant destroy' then 'vagrant up' in the directory where you setup your virtual machine. (In the case of QEMU, redownload the qemu-499.zip file, extract it, and boot from scratch.) Now the scp should succeed. If this is successful, you can just work from the new image. You should have all your files in the thoth machine so it should not be a problem.
Q) VirtualBox fails to install on my Mac.
A) New versions of OS X requires users give explicit permission when installing a kernel extension (which VirtualBox requires). Go to your System Settings folder, then your Security and Privacy folder, and there will be a message asking you to give permissions for VirtualBox to run under the General tab. Allow it to run.
Q) I get an error while doing 'vagrant up' involving curl.
A) Try doing the following on your Mac command line:
sudo mv /opt/vagrant/embedded/bin/curl /opt/vagrant/embedded/bin/curl.bak
The problem is that the curl distributed with vagrant is incompatible with some Mac systems and that's why it is not able to download the Debian image. Once you do the above command, the Mac machine will revert to its original curl, under /usr/bin/ and everything should work out fine.
Q) 'vagrant up' works fine and CentOS installs correctly. But when I try to launch it, I get errors such as Intel VT-x missing or AMD-V missing.
A) This is because the virtualization support in your CPU has not been turned on by your BIOS. Please follow instructions in the related link under Project 4. If all fails, please follow "QEMU / Tiny instructions".
Q) My machine is very slow or has very small memory, and VirtualBox is running way too slowly.
A) Please follow "QEMU / Tiny instructions".
Follow these instructions to build dice_dev.ko:
tar -zxvf ~wahn/public/cs449/dice_dev.tar.gz cd dice_dev make -f Makefile.qemu ARCH=i386
When compiling rpg game, you need to add the '-m32' option to gcc to create a 32-bit x86 binary since QEMU is emulating x86. You also need to add the '-static' option to perform static linking to remove any library dependencies, since thoth and QEMU / Tiny have different library versions installed. E.g.:
gcc -m32 rpg.c -o rpg -static
When sending signals to rpg game, you also need to make the following changes. Thoth and QEMU have the same signal names but different signal numbers. If you do 'kill -l' on both machines, you will find that on thoth it is number 34 but on qemu it is number 33. So instead of:
kill -SIGRTMIN <PID>
do:
kill -34 <PID>
Q) If a define a local array sized count (e.g. unsigned char temp[count]) and fill that array with numbers, why do I get a kernel crash?
A) By defining unsigned char temp[count];, you are defining a variable-length array. Meaning the array will be sized by count and the compiler will generate code to allocate kernel stack space to accommodate that array. The problem is, the kernel stack space is limited to 8 KB (by default) to minimize the kernel memory footprint. Hence if the array exceeds the kernel stack space, it is going to overflow the kernel stack and updating the array will start overwriting kernel locations beyond the stack. This is what causes the kernel crash. So you have to do one of the following:
Use a smaller array on the stack sized to be less than 8 KB (e.g. 100), and call copy_to_user every 100 chars to copy that array to the user buffer. At the extreme, you just need a single char local variable if you are prepared to call copy_to_user count number of times. Calling copy_to_user this many times is obviously not efficient but it works and you will get full points.
Use an array sized count bytes but allocate it on the kernel heap using kmalloc() and free it later using kfree(). Since the kernel heap does not have the 8 KB limitation, this will also work. This needlessly allocates kernel heap memory when you can do 1. But it will work and you will also get full points.
Q) What is copy_to_user and why do I need it?
A) See the below prototypes for copy_to_user and copy_from_user:
/* Copy data from kernel space to user space. Returns number of bytes that could not be copied. On success, this will be zero. */long copy_to_user(void __user *to, const void *from, unsigned long n);/* Copy data from user space to kernel space. Returns number of bytes that could not be copied. On success, this will be zero. */long copy_from_user(void *to, const void __user *from, unsigned long n);
They are both basically doing a memory copy of from -> to. But before doing the copy, they perform a check of the user space pointer ('to' in the case of copy_to_user and 'from' in the case of copy_from_user). They check that the pointer is indeed a pointer in user space (not in kernel space) and it is okay to read or write from that pointer. This is to prevent malicious (or clueless) users from invoking a system call passing a pointer in kernel space (masquerading as a user space pointer) thereby accessing kernel data.
Case in point, try compiling the below tester program and running it with your device driver. Obviously the program is buggy since you should never pass a NULL pointer to the destination or source buffer of read and write. However, it should not cause a crash or malfunction in your kernel. If the below code does not cause a crash, congratulations, you have written a secure and reliable device driver. Otherwise, you should consider changing raw accesses of user space pointers with proper calls to copy_to_user and copy_from_user. Note: another cause of a crash may be a divide-by-zero in the random number generator when you do the modulus. I will not perform this test on your /dev/dice since it is not included in the grading rubric.
#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int main(){ int fd = open("/dev/dice", O_RDWR); read(fd, NULL, 1024); write(fd, NULL, 1024); return 0;}
Q) What is KDIR inside the Makefile?
A) KDIR is the directory that is passed to the -C option in another invocation of make. As we learned the -C option changes into the given directory before doing make. Under the directory given by KDIR, you will find another Makefile with the modules target inside. KDIR is short for kernel directory and contains an installation of a kernel that the device driver is compiled and linked against. The kernel version should match the one running on top of the virtual machine.
Q) How did we get the .ko extension and what does that mean?
A) The .ko extension is short for kernel object and the extension is used for object files that have been linked against the kernel and can be dynamically loaded into the kernel as modules. The generation of the hello_dev.ko file is done by something called a Makefile, which is a type of script for building programs. When you do 'make', this invokes a Makefile that is in charge of the build process, invoking the gcc compiler and the ld linker with appropriate options. We will learn more about Makefiles during the lectures.
Q) Why does it say command not found when I try run insmod?
A) That is because you are not logged in as root. Either log in again as root, or do 'sudo bash' to gain root privilege. Only the root account is able to run insmod because that allows you to modify the kernel. Q) So how does 'cat /dev/hello' actually produce 'Hello, world!'? What's going on behind the scenes? A) 'cat' is a program that simply keeps on reading a file until it hits EOF and dumps its contents to stdout. Here are the steps involved:
'cat' does a read system call to fill in a char array buffer
/dev/hello is not a real file but a character device file under DevFS, so the read system call results in a call the device driver read function in the file operations table.
The arguments to the read system call is passed to the device driver read function as follows: 1) 'file': pointer to a file struct, which is the result of indexing into the kernel open file table using the file descriptor passed to the read system call, 2) 'buf': the user buffer passed to the read system call, 3) 'count': the requested number of bytes to read passed to the read system call, 4) 'ppos': an internel pointer to the current file position for each file in the open file table.
Inside the read function, 'ppos' is initially zero, so we go past the if statement and then copy_to_user is called. This will copy the hello world string to the provided user buffer. Then 'ppos' will be advanced by the string length, and the string length will be returned as the number of bytes read.
Eventually, the read system call inside 'cat' will return and by now the provided buffer has been filled with the string and the call returns the length returned by the above read function. So 'cat' will dump the buffer to stdout, the number of bytes returned by the call.
Subsequently, 'cat' will do another read system call to read more bytes from the file, but this time the device driver read function will return 0 because now 'ppos' is non-zero. And all following calls to read will return 0, unless the file is opened again which will reset the current file position to 0.
A return value of 0 from a read system call means EOF has been reached so 'cat' will close the file and exit.
Q) How do you return a continuous stream of random bytes?
A) Your device driver 'emulates' a hardware device that generates random numbers. Now, if such a device existed, and you were to write a device driver for it, you would want the user to be able to open the device once and read as many random bytes as she requires, and then close the device. Just like if you were to have a microphone device, you would like the ability to open it only once, read in all the stream of sound recording, and then close it. You would not want to read in one byte, close the device, and the open it again before you are able to read the next byte. If you see the above output of 'cat', 'cat' keeps on reading in bytes as long as there are bytes to read, and the dice driver keeps on producing bytes if asked. That's why you get an infinite stream of bytes in the output which needs to get truncated using 'head'.
Now, how do you return a continuous stream exactly? The hello device driver for one does not return a continuous stream. The hello_read function returns 0 if *ppos is not 0, meaning that the second time hello_read is called, it will return 0. That will tell the application that the end-of-file (EOF) is reached and the application will not be able to read from the device anymore. (Try 'man 2 read' on the thoth machine, to find out how return value 0 signals EOF). So, in your dice device driver, you want to do the following:
Modify code such that the device driver read function always returns the number of bytes read, regardless of how many times called.
You will not have any more use of ppos. In a storage device with a concept of current file offset, you will want to maintain ppos (try reading LDD Chapter 3: Char Drivers for more details). But the dice device does not need it.
Q) How many bytes should the dice device driver read?
A) The device driver should read as many bytes as given in the 'count' parameter of the call, as explained in the comments. Some students have asked, what if count exceeds the size of the buffer passed? It is the responsibility of the user to size the buffer correctly. If the user does not, and the device driver happens to overflow the buffer and corrupt memory, it would corrupt the memory of the application but not the kernel. So it would just be the user's loss.
Q) Why should the device be able to read multiple bytes? A) A user program (like RPG game) may choose the read just one byte at a time. But it may also choose to read many bytes at once into a buffer and then use numbers from that buffer. In fact, the latter method is often preferred to reduce system call overhead. For example, the 'cat' program does not read one byte at a time from a file, it reads many bytes at once into a buffer, for the same reason.
Q) What is "struct file * file"?
A) See above explanation on 'cat'. I refer you to the section "The file Structure" of LDD Chapter 3 linked above, for more details.
Q) What is "loff_t *ppos"?
A) I refer you to the explanation on the "loff_t f_pos" member of the struct file explained in the "The file Structure" of LDD Chapter 3 linked above.
Q) How do I properly register a signal handler?
A) There is an example on the System Call and Signals slides that uses a SIGALRM signal. There is also another example of handling a SIGSEGV signal. Also please try doing 'man signal' to read more about the usage.
Q) How do I write to the device driver a binary number to set the number of sides?
A) We learned that device drivers exchange data with the outside world using a file interface, which in this case is /dev/dice. So you simply have to open /dev/dice and write to it. All write system calls will be forwarded to the device driver for /dev/dice, or dice_write. Review the Hello World example program at the end of the System Call and Signal slides on how to use write system calls.
Q) My rpg program would not die on 'kill <PID>'
A) This is probably because you handled SIGTERM incorrectly and did not exit from the program. To force kill rpg, you should send it the SIGKILL signal by 'kill -SIGKILL <PID>'.
Q) How do I send signals while I'm running an RPG game? All my input will go to the stdin of RPG game and I will not be able to send signals.
A) On thoth, you can launch a new terminal simply by starting a new SSH session. On the virtual machine, you can press Alt+F1, Alt+F2, ALT+F3, etc. to switch between terminals. You are in terminal 1 by default, so if you press ALT+F2, you will get terminal 2 where you can login again and start a new session. Now you can start sending signals from that terminal. You can always switch back to the original terminal by pressing ALT+F1. In order to send a signal, you will need to discover the PID of the RPG game. If you do 'ps -u' on the command line, it will list all the processes that you launched, including the ones on other terminals. After you get the PID, you can send a signal by doing: 'kill -SIGRTMIN <PID>'.