Write a Shell in C
brennan.io
brennan.io
For the longest time I thought the "way to do that" probably involved some crazy sshd config or something, but then the penny dropped and I realized "Well shit, you could just make it a shell and set the user's shell entry to that program. Duh."
If somebody knows a better approach to that sort of thing, I'd love to hear about it. But for now I'm content that I've found an approach that seems workable.
(and if you're wondering more: No, this isn't anything interesting, or a startup project, or anything you'd probably even see on Show HN. It's just an example of the kind of random crap I think about in between doing other things.)
https://github.com/KSP-KOS/KOS/blob/develop/src/kOS/UserIO/T...
Unfortunately, it appears the solution was to write an entirely custom telnet server. Author also says in the included documentation that SSH was the original plan but implementing all of SSH is far too complicated.
A couple of succinct examples I found via a quick google:
https://www.virtono.com/community/tutorial-how-to/restrict-e...
https://ctrlnotes.com/restrict-a-user-to-ssh-forced-command/...
You could also use Match in sshd_config and ForceCommand too.
Funny you would mention that... using a curses or TurboVision based TUI app is kinda what I was thinking about that inspired this hole avenue of exploration.
Innocent times.
Please don't do this. sizeof(char) is always and everywhere 1.
More importantly, don't do:
<type> *buffer = malloc(n * sizeof(<type>));
which frequently leads to bugs of the form
<new_type> *buffer = malloc(n * sizeof(<old_type>));
Instead, use
<type> *buffer = malloc(n * sizeof(*buffer));
which will always keep both sides consistent.
PS: See also http://c-faq.com/charstring/wchar.html
Of course, it doesn't work when you're doing things like OOP in C, where the type on the left is a "superclass", or using flexible array members and allocating more than just a header's worth.
In C++, you can make a very simple REPL that is superior to the one in the OP in almost every way.
#include <iostream>
#include <string>
int main() {
std::string input;
while (true) {
std::cout << "> ";
std::getline(std::cin, input);
if(input == "exit") {
return EXIT_SUCCESS;
} else {
std::cout << "Unrecognized command: " << input << '\n';
}
}
}
Code quoted from “Setting Up A Better REPL” (https://mikeloomisgg.github.io/2019-04-10-set-up-repl/). It offers good insight into why C is bad in light of the alternatives like Rust and C++.C has free, which deallocates a block of memory pointed to by a pointer. If it's a struct, the struct is deallocated. If the struct contains pointers, that memory is still there. If you've lose all the pointers to that chunk of memory, you now have a memory leak.
It is not uncommon to make your own allocators and deallocaters for complex structs.
And in Scheme, you can make a very simple REPL that is vastly superior to the one of yours in every way. But I suspect that neither fulfills the goal of learning how things are done in C, which seemed to be the goal of the author of the article.
Using bit-shift operators for input/output is a heinous crime against human dignity, basic decency and social order. Every use of it should be punished with 10 lashings, and the promoters of this atrocity should be tarred and feathered.
I'm agnostic on C++'s std stream stuff but I am pretty anti-"yes bitwise logical operators are so important that they get priority over all these symobls".
- wrong arguments types (mostly solved by libraries such as fmt tho): printf("%d", 0.5);
- too much / not enough arguments passed: printf("%d", 0, 1);
- passing unsanitized strings as template argument: printf(some_string_i_got_from_the_network_lol, arg_1, arg_2);
the << method makes these three errors impossible.
Also << does not allow you to have something like printf's %n, which writes to one of its arguments.
fmt is able to give compile-time validation of string literals, but at a hefty compile-time cost. In my opinion the best solution would be string interpolation, but we're definitely not there yet...
> - too much / not enough arguments passed: printf("%d", 0, 1);
-Wformat
> - passing unsanitized strings as template argument: printf(some_string_i_got_from_the_network_lol, arg_1, arg_2);
-Wformat-nonliteral
C gives you many ways to shoot yourself in the foot, so you need to make sure to use available language features to your advantage.
also, calloc is a nice idea too
The extra parentheses are there for the us humans who might need help distinguishing between * as dereferencing operator versus * as multiplication all happening within the space of eight or so characters.
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf (Annex E)
It seems like you are conflating things. One is, a char does not have to be an octet. That is correct.
The other is the unit for sizeof(char) is always and everywhere 1. That is, the size of everything else is measured relative to char.
Please do see C FAQ[1]. In addition, please see 6.5.3.4.2 in [2]:
> When applied to an operand that has type char, unsigned char, or signed char, (or a qualified version thereof) the result is 1.
It does not matter if char is 11 bits therefore not an octet. That means you are in a universe where a byte is 11 bits long, sizeof(char) is still 1 as far as your program and the compiler are concerned.
Note that the same is true in C++ as well:
> Depending on the computer architecture, a byte may consist of 8 or more bits, the exact number being recorded in CHAR_BIT. sizeof(char), sizeof(char8_t), sizeof(signed char),sizeof(unsigned char), and sizeof(std::byte) are always equal to 1.
HTH.
[1]: http://c-faq.com/charstring/wchar.html
[2]: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf
So.. by the C standard, a byte is a size of char.
<type> *buffer = calloc(n, sizeof(*buffer))
as that will also protect against multiplication overflow.> So naturally every existing calloc out there performs this overflow check and doesn’t just multiply its arguments together? Oh no. Even I was surprised.
> On OS X (10.4.11) calloc(65537, 65537) returns a non-NULL pointer. This is a bug.
> PHK’s code in NetBSD is typical, it just multiples the two arguments and uses the result, reckless to overflow.
> Hurray for FreeBSD: they do almost exactly what I suggest, but with a fast test that is just the tiniest bit unportable.
Which suggests you shouldn't rely on there being a multiplication overflow check.
[1] https://drj11.wordpress.com/2008/06/04/calloc-when-multiply-...
When I was new to systems programming, that lab — along with all the other labs part of "Computing Systems from a programmer's perspective, especially the binary bomb — helped me better understand lower level concepts such as debugging assembly, fork-exec, etc.
bufsize += LSH_RL_BUFSIZE;
Shouldn't that be: bufsize += LSH_RL_BUFSIZE * sizeof(char); buffer = realloc(buffer, bufsize * sizeof(char));
This makes the code symmetric wrt. the malloc() call. And better allows the code to be rewritten when moving to e.g. a wide-character type.char is always 1-byte according to the C standard. (Actually, I think it's more like the smallest addressable unit is 1-char, but it's been I while since I've read the spec)
Yes, but sizeof char would still be 1, unless I'm mistaken.
I think I might have made a major pedagogical mistake when I declared char == 1 byte. What I meant was that when you're working in C, a char is the smallest addressable unit. Now, that smallest addressable unit is conventionally known as a byte, but it might not be 8-bits wide. These days it is almost always is thankfully.
Thank you for clarifying that, because I don't think my original comment was clear enough. I've been meaning for a while to write a blog post on the origins of 8-bit bytes as well as other conventions in CS.
// old reply:
I already replied to nanis above, so i+l just c/p it here.
CHAR_BIT is a minimum of 1 byte (8 bits), but is not guaranteed to be 1.
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf (Annex E)
> Their implementation-defined values shall be equal or greater in magnitude... - ISO/IEC 9899:201x, Annex E, 5.2.4.2.1
The quoted part of the standard doesn’t talk about sizeof(char). Sizeof returns the size of its operand in char-sized units, therefore sizeof(char) is tautologically 1.
> bufsize += LSH_RL_BUFSIZE * sizeof(char);
sizeof(char) is always and everywhere 1.
Although this seems to be missing a few things like & and pipe, I find it very interesting how so many people can Implement a shell differently with the same C syscalls.
The Linux man-pages project[1] tends to do a pretty good job of telling you when different libcs got a given function, or compatibility concerns between different libcs. BSD man pages tend to just tell you where they copied their implementation from. But unfortunately the Linux man-pages getline(3) page only told me that "Both getline() and getdelim() were originally GNU extensions. They were standardized in POSIX.1-2008.". (I guess that saved me from searching my POSIX PDFs to determine the POSIX version. While I was a student and could legally aquire such things for free I hunted down every version of POSIX or SUS prior to 2016).
I checked the GNU libc git[2] to determine when GNU first got the function, but didn't quite get a clear answer, becuase much of the history prior to 1994 was mangled in the conversion from whatever they were using before to RCS (which they used before they converted to CVS, which they used before they converted to git). I didn't bother checking the GNU manual; it doesn't tend to do a good job of saying when things were introduced.
Next I checked macOS; the macOS 10.15 (Catalina) getline(3) man page confirmed that it appeared in POSIX.1-2008, and also told me that "These routines first appeared in FreeBSD 8.0" ("these routines" referring to their specific implementation of those routines.). I checked through opensource.apple.com archives[3] (actually, I used an import of those archives in to git[4]) to determine which version of macOS they first appeared in.
I checked the FreeBSD man page archive[5] to confirm that they first appeared in 8.0 (a getline(3) man page exists in 8.0 but not 7.4).
I checked the OpenBSD man page archive[6] next. In recent versions the "HISTORY" section says "These functions were ported from NetBSD to OpenBSD 5.2.", though that section doesn't exist in the 5.2 version of the man page. And I confirmed that no such man page exists in 5.1.
Finally, I checked the NetBSD man page archive[7], and bisected through the versions to determine when it was introduced.
I used Wikipedia to determine the release year for each of those versions.
[1]: https://www.kernel.org/doc/man-pages/ [2]: git://sourceware.org/git/glibc.git [3]: https://opensource.apple.com/ [4]: https://github.com/apple-open-source/macos [5]: https://www.freebsd.org/cgi/man.cgi [6]: https://man.openbsd.org/ [7]: https://man.netbsd.org/
A nice, simple, well-documented shell in C!
Not too little, and not too much.
Just the right balance between functionality and complexity for education.
Favorited!
https://github.com/angrave/SystemProgramming/wiki#2-processe...
I often wonder how modern run loops work. They don’t burn cycles?
The main loop tries to read characters using the getchar() function, which in turn pokes them out of a buffer inside the "stdin" FILE struct.
If the buffer is empty, somewhere inside the libc, it calls the read() system call on the standard input file descriptor to get more.
If the kernel doesn't have any more data to return, the system call blocks. I.e. the kernel marks the thread as waiting for I/O and no longer schedules it. Once the kernel has data, the thread is scheduled again and the read() system call returns.
And before the obvious follow up question comes: If all programs are blocking on I/O and the kernel doesn't have any threads to schedule, it schedules an "idle thread" that spends its time slice throttling down the CPU. The CPU usage that process monitor programs are displaying is basically the amount of time a CPU core is running something that isn't the idle thread.
Specifically (AFAIK, at least back then) on x86 the kernel schedules an interrupt and sends a single HLT[0] instruction, and only wakes up when the interrupt triggers. A smartly set up interrupt would have the CPU wake up only as needed for what is required to unblock the pending blocked operations.
Smarter schedulers would go as far as coalescing active events together to rush to idle yet wake up less often, and spend as much time down as possible in increasingly deeper C-states[1].
Back in the day some kernels would loop over NOP[1] instructions, so the CPU would effectively never sleep!
[0]: https://en.wikipedia.org/wiki/HLT_(x86_instruction)
printf("a Shell");
Toolchain is pretty similar, often more performant, you protect yourself against memory leaks, dangling file descriptors as such (yes, without needing a garbage collector [1]), and some decent libraries to help you out with what you're not writing yourself.
(Potential answer: Bigger executable if you're not careful. In some cases that could be a consideration. [2])
--
[1] : https://stackoverflow.com/a/48046118/1593077
[2] : https://softwareengineering.stackexchange.com/q/246167/63497
and some decent libraries to help you out with what you're not writing yourself.
If you're writing a shell you are probably already somewhere on the "bootstrap pilgrimage". Using something someone else has written is the exact opposite of what you want to do in that case. (Why not just use a shell that someone else has written then...?)
I don't envy anyone writing such a compiler. Its possible but I didn't start one. Seemed like less fun and more like red tape.
The pilgrimage thing is real. Done it myself. The journey from even boot to bash is real and the benefits of awareness it brings definitely appear in strange places. I went even further before the "boot" stage.