In my last blog post, I dove into some of the code behind the sudo command. I thought this was pretty fun. sudo is one of those commands that I use quite often but havenât had the chance to look into truly. I started thinking about other commands that I use on a daily basis but had little understanding of the internals of. The first command that came to mind is cd. cd stands for change directory. Simply put, it allows you to set your current working directory to a different directory.
$ pwd ~ $ cd dev $ pwd ~/dev
So, the first thing I did was figure out what exactly was invoked when I ran cd on the command line. I used the which command which displays the path of the binary associated with a particular command.
$ which cd /usr/bin/cd $ cat /usr/bin/cd #!/bin/sh # $FreeBSD: src/usr.bin/alias/generic.sh,v 1.2 2005/10/24 22:32:19 cperciva Exp $ # This file is in the public domain. builtin `echo ${0##*/} | tr \[:upper:] \[:lower:]` ${1+"$@"}
Oh, bother! Reading shell scripts can be such a hassle sometimes. I know the tr command is used to translate characters. In this particular case, the second half of the command, the part after the pipe symbol, basically converts the command cd dev to CD dev. I have no idea why this is. In any case, this modified command is passed to the builtin command which is handled by the shell (Bourne shell) that we are using.
I decided to dive into the code for the Bourne shell to see what I might be able to figure out about these builtins. I came across the definition of the cd builtin here. Iâll admit that it was a hassle to read this file. For one, the source for Bash does not have a mirror on GitHub, so I had to browse through a hosted version with a somewhat cumbersome file browser.
Nonetheless, I read through some of the code that was defined in this file. Some of it was in functions, and other bits were in templates, but after a while, I figured that most of the code was a wrapper around a function called chdir. A lot of the functions defined in the cd.def file linked above actually just invoke chdir and handle errors and parameter cleaning.
I did some Googling on what the chdir function is. Itâs a function that is a standard part of Unix. So to figure out more about how chdir worked, I had to dive into the code for the Linux kernel which can be found here.
I started, as I usually do, by searching for the term âchdirâ using the GitHub search bar. I find that it is accessible and useful. After scrolling through some of the results, I realized that OMG something I learned at university was going to come handy to me.
The chdir function implements a system call. Whatâs a system call? A system call is a process by which a user program (like Bash) requests access from the kernel to execute a command. Usually, this is done for commands that require a certain amount of privilege. Changing into a directory is a privileged command from the operating system perspective. Youâre essentially giving the user access to a new portion of the file system on the machine so it makes sense that programs would have to pass that request to the kernel for security reasons.
So all in all, here is what happens when you run cd on the command line.
The cd builtin is invoked as part of the Bash shell.
The Bash shell invokes the chdir function.
The chdir function is part of Unix and invokes the chdir system call.
The Unix kernel executes the chdir call and does its own low-level thing.
I could dive in a little bit more into how #4 works, but letâs be honest, Iâve already read too much code at this point, and my eyes are starting to hurt.
I hope reading this was illuminating for you!
UPDATE: After reading this blog post, Julia Evans (@bork), provided the following insights on why cd is implemented the way it is through an email.
So!! Why is cd a builtin function in bash and not a program? Some builtins (like time) are bash builtins but they would also work as standalone programs. Is cd one of those?
It turns out that cd HAS to be a shell builtin otherwise it wouldnât work! Hereâs why:
Every process has a set of attributes that the Linux kernel stores. These attributes are things like environment variables, signal handlers, and â the processâs current working directory!!!
Processes arenât allowed to change each otherâs attributes (if Iâm a program, I canât change the working directory or environment variables of another process).
SO!! If you had a /usr/bin/cd program that ran chdir, that would be fine, but when you started it it would change its own working directory and exit which is not very helpful. It wouldnât change the working directory of you (the parent process)
So bash has to call the chdir syscall itself which is why itâs a builtin.