As a system programming language, C still deserves learning today
nanxiao.me
nanxiao.me
> My main idea is that we need to teach C in a way that helps students understand why a very large fraction of critical software infrastructure, including almost all operating systems and embedded systems, is written in C, while also acknowledging the disastrously central role that it has played in our ongoing computer security nightmare. [...]
> We’d like students to be able to answer the question: Is C an appropriate choice for solving this problem? [...] The second big thing each student should learn is: How can I avoid being burned by C’s numerous and severe shortcomings?
The only solution is using a program analyzer which can prove the absence of certain errors. Frama-C and Astrée come to mind.
Normal static analyzers are best-effort, will have both false positives and false negatives and will also miss errors. This is the difference between testing and verification.
Dynamic analyzers like ASan or valgrind depend on having good code coverage and good value domain coverage. The former should be obvious, the latter will ensure that error-prone code which fails only when variables have specific values is also flagged. E.g: adding two ints is defined for some value ranges, undefined behaviour for others.
Now the main issue with all of the above is that virtually no self-respecting C programmer will use a verifier, so while theoretically possible to write safe C, it is practically almost unheard of - safety-critical domains aside.
There is no good safe, user-friendly alternative to C, all of them are either much more complex languages (Rust, C++), or less performant (Swift, Go) or have tiny marketshare (D, obscure things like Zig, etc)
"C++ ecosystem: For better, for worse - Anastasia Kazakova"
https://www.youtube.com/watch?v=43E5iYzrQn4
Although a C++ talk, she also touches C quite a few times, given the goal to get them to adopt C++ as safety improvement, and naturally how to tailor CLion to the markets they are on.
Much of Rust's complexity is not directly addressing memory safety, it's providing high-level language features. A C-with-borrow-checker would be very different from Rust.
Sure, Rust itself has a number of higher-level features, some of which are a bit problematic by comparison with C (e.g. monomorphizing generics lead to intermediate-code bloat, which causes long compile times) but it turns out that these features are practically needed, either to make programming-in-the-large more feasible or to abstract some underlying details from user-level Rust code so it can keep working even as the compiler and standard library evolve underneath it. C doesn't have these problems being a mature platform, but Rust still does.
I think this is where we start bumping up against the limitations of the universe we live in, because the question becomes "where do we put the bugs?".
The biggest flaw with C is the array bounds checking, especially if that array is on the stack, or other bugs related to "raw" pointers:
- We could enforce things through hardware, but then you've put the bugs in the microcode.
- You can change your language, but then you've put the bugs in your compiler / interpreter / vm.
- You can try and formally verify the correctness of your code, but it's less feasible for large codebases. And what if there's a bug in your proof? (proofs are programs)
I'm not suggesting people use C/C++, but I wouldn't blame C for the problem of natural numbers.
Sure, but the number of bounds-checking bugs in, say, the Rust compiler/stdlib is multiple order of magnitudes lower than if all Rust applications had to roll their own bounds-checking code.
Precisely that which makes C so performant is also that which leads it to be "unsafe". It is thus up to the programmer to ensure safety in C, his manual managing of that is the price to pay for the performance gained using the language.
I'd rather put my bugs in one, more easily verifiable place. Verifying all code that's ever going to be compiled is close to impossible. Verifying the compiler / interpreter / VM is hard, but it's much easier.
At the end of the day our field is basically: "get rid of repetitive work". So arguing that it's better to have total manual control (and basically always re-implement things at the point of use) doesn't strike me as the best approach.
memory safety issues account for an overwhelming amount of security bugs¹ and are avoidable by moving to managed languages. Compilers and VMs are very, very good at elimianting this primary source of bugs in code.
The first answer to the C/C++ problem should be to, whenever possible, move to languages that do a large part of the work for you.
In particular in the linux world it seems like a lot of application code that could be written in memory safe languages is still written in plain C.
[1]https://www.zdnet.com/article/microsoft-70-percent-of-all-se...
When you read about people who have had problems with C, you will find that these are people who never really learned the language and the language is as simple as one can get which should also make one question their abilities.
People coming with "no bound checking/type safety/pointer safety/garbage collection in C" often don't get the simple idea they weren't supposed to be there to begin with, and in C the burden of doing in falls on the developer.
There is nothing wrong with setting types manually, managing memory manually, and thinking through how pointers and array boundaries are computed. In fact this is what makes C unbeatable in its niche, and leads to software with very low resource and performance requirements.
Aka, C code is doomed to segfault. The routine easily verifiable work should be done by a machine, aka by the compiler.
The first batch would happen in any language, it's an operational issue.
The second batch comes from C and would likely be prevented by PHP being written in idiomatic Rust.
The third batch is a higher level development issue that is cross-language.
I think there's value in removing classes vulnerabilities.
And blaming the operator is NEVER a good idea. It was the exact opposite of this mentality that made us have safe cars and airplanes today.
Before the 70's, the auto industry was doing exactly what you're doing. Once they stopped doing that, people stopped dying for easily preventable issues.
Each of these aspects of the "C ecosystem" needs to be well understood in order to be a productive, high-quality C developer. Its not enough to just type a bunch of stuff and then throw it at the compiler and see if it works - you have to understand what the compiler is doing with your language constructs and how this will be executed in the target execution environment.
So many times I've had to debug "professional C developers" who have no clue what the compiler is doing to their code, no idea what a TEXT SEGMENT is, absolutely zero responsibility for the HEAP, let alone runtime loading and linking. Its all just 'voodoo' behind the 'black box' of the compiler.
But even just having a basic understanding of these components can mean a huge difference in quality C code.
Another thing every C developer needs to know: how to debug code and read assembly language in the context of the operating/execution environment. You don't need to be able to WRITE assembly, but at least fire up the debugger and step through your program a few times to see how it behaves .. this can go a long way towards increasing a C developers understanding for what is happening and why its important to know all the other components. Too many times I've solved a major, delaying bug in a project by just re-ordering the linker commands or, even worse, cleaning up the linker commands to remove stuff that was glibly thrown at the wall by some other dev.
Also - all warnings are errors: no exceptions.
Part of me misses working on easily googleable things.
But this is a something that even experts fail to do.
https://github.com/google/sanitizers/
Sure, Google is primarily a C++ shop, you could say that C++ is to blame and it has nothing to do with C.
But why the need for KASAN then? Which has a big track record at this point, by the way.
Mozilla decided that it was such a difficult task in C/C++ that they created their own language.
> that overflow is as severe as memory bug in C/C++
In practice, it isn't. In many traditional compilers it has predictable behavior (two's complement wrapping), if we're not talking about floating-point overflow.
Some programs explicitly rely on it. Compiler support can be provided for those programs.
It's simply not in the same category as memory corruption bugs.
Of course, ISO C and C++ have just one category for undefined! However, note that "undefined behavior" is a formal term which extends over beneficial areas such as documented extensions and the use of third-party libraries and headers.
Okay, you were serious about safety. Congratulations, you are the first one I have ever come across.
I have never seen anyone else wrap integers in a class in order to use them with stable semantics.
Twenty years ago, C++ was still hot and there was a lot of interest in all sorts of techniques. Books, seminars, papers, blogs, you name it.
There is a way to use C++ template partial specialization to mimic the built-in conversion rules, like "int op long" promoting the left operan to "int". You can mirror the language in itself and bend the rules.
I hadn't ruled out calling outside platform API functions, which were all written in C. You can't do that in any language, unless you're writing a pure text filter or calculator for the Unix command line environment (and don't count the I/O and math functions).
I still use C++ as one of my favourite hobby languages and althought it has improved a lot, using C++20 best practice across a team (lets assume it is already available), with binary dependencies, is still a challange to make it 100% memory safe.
It was 100% written by me.
Note that the Rust devs made an entire 100% written-by-them-language to make the same claims.
In C++ static analysis is optional, while it is part of language in Rust.
Then there is the whole language culture.
While me coming from stronger system languages, always strived for bounds checking enabled on my own C++ projects, good luck selling that to most C++ teams, even though in 99% of the use cases its impact is negligible.
Finally going all the way back to NEWP, system languages that require explicit unsafe blocks are much easier to do code review, than those where every line of code can possibly trigger unsafe behaviour, and C++ inherited lot of such cases from C.
It's a tiny language, has easy syntax, and "undefined" behavior (which you don't normally run into) exists for a reason -- e.g. to avoid having to check for unlikely cases every time a heavily used function (e.g. memcpy) is called.
And that's fine, why does one have to understand an ancient language when they can interact perfectly fine with machines by using Python, Swift, Java etc?
That's why I included Java, C# or Javascript. If you can read those languages you can read C.
And it's not the language what they need to understand. C in itself is a fairly simple language, that's why I said that any person with experience in that family of languages would be able to read it straight away. What one should understand is how the computer is doing what you ask it to do. What does "create an object" mean, what is a "reference to an object", what are the heap and the stack, etc. These are concepts that are extremely useful and, as the parent comment said, almost required to know in order to be a full programmer and be able to understand what happens when you create your programs.
I've been doing C++ for more than a decade and had two tough experiences lately:
* I had to review a C project of several thousand lines. It's impossible for a human to understand if memory management is done correctly without weeks of deep analysis, so I had to resort to tools. The logic of the code is also hard to make out because of the memory bookkeeping.
* I was reading the open source code of a Linux program and had to dig deep into a couple of system topics and read the documentation for half a dozen syscalls to make sense of it.
For me, C is one of the hardest languages to understand, you're always zoomed in at 10x, looking at all the insignificant details of memory management, working with strings and arrays, etc.
I think that's what the parent comment meant with "reading (normal) C". The language itself is simple, because it only has the ifs, elses, and fors. Of course, given the simplicity of C the projects end up being complex because they have to manage a lot of things that other languages do for the programmer, but that is outside the scope of the language (i.e., you don't need to resort to the language manual to understand it).
In other words, the discussion is not that everybody should be able to dive deep into C projects and instantly know what's happening, and know all syscalls and everything. The discussion is that a programmer should know what is memory management, what is a system call, etc, which is practically synonymous to being able to read normal C.
{
while (*dest) dest++;
while (*dest++ = *src++);
}This is pretty much C specific. And side-effect ridden, at that :-)
I doubt the average Javascript programmer will figure that bit out, at a glance.
If you can read those languages, you can probably tokenize C fairly well, and parse a decent proportion of it. But only understand maybe a small fraction.
If the interpreter or VM used by any of those languages is written in C, someone still needs to understand it. Not to mention that it doesn't hurt to understand the internals of your chosen language if you want to make the most efficient use of it.
Doesn't hurt to know the internals, but one's time's limited and better spent on other endeavors. If I wouldn't be doing embedded I wouldn't touch C with a 3m pole, nor would I need to :) In fact my life's mostly C free nowadays and I'm quite pleased with that.
It's stopped evolving and can't really shine in any of today's domains.
It will still be around for decades, but mostly as legacy. I.e: not anything which is required to be learned.
There's also those that misguidedly use C when they shouldn't (e.g to speed up Python code). It's unfortunate, but such is C's siren song - mesmerizing one with promises of speed only to be dragged into the depths of undefined behavior.
Here's what I know:
* AI / ML can be done in many languages, but no one's seriously considering C, despite the performance requirements. C++ is sometimes used.
* self-driving companies are hiring for C++.
* Mobile is Swift / Kotlin with C++ for performance-intensive code where appropriate.
* Web is anything except C and C++ (thankfully)
* Game development is C++
* The enterprise has switched to web technologies or is still using Java/.net.
* The desktop varies, but it's mostly .net / Swift + Obj-C / C++, with the exception of gnome perhaps, which I'd argue is an anomaly.
C is still holding on in kernels and low level embedded, where it's expected that C++ and maybe Rust will continue to squeeze it.
Personally I don't care that much about the above two. Banging the same bits for decades gets tiresome.
I'd go into this more but I don't feel like explaining these things to people ad nauseum. C is one of the most used languages around the world, in new projects, too, and nothing you've said changes that.
Some of the things I listed are C++, which is a different language, which kept evolving unlike C and also unlike C is in demand today for all sorts of hot domains. The two can't be mixed.
But in a way, you may be right ... all the critical new lower level programs like web servers are now mostly written in Go, Rust and C is not favourably considered for them ...
> misguidedly use C when they shouldn't (e.g to speed up Python code)
The Python virtual machine itself uses C to speed up Python code. Python's compiler is even written in C instead of being properly self-hosted.
C has nothing to do with this. C is a quirky old high-level language which has nothing to do with underlying abstractions, i.e. with how the machine works.
If you want to learn what's beneath, you open a machine manual and read it.
You have to be a complete engineer because you take full responsibility for what you're doing. All your tools carry disclaimers in their licenses which absolve them of any responsibility if something goes wrong.
for LANG in Python, Ruby, Rust, Java, ... :
If $LANG has a bug, and because of that your $LANG program causes some harm to the customer's data, you can't blame $LANG.
If I'm in a situation that I'm somehow required to do something with Python, and something goes wrong, I can debug it right into the Python internals. If the problem is with how GCC compiled $LANG, I can debug that too, and I can drop to the machine level if needed
My point being, that it is a bit arrogant claiming that somebody isn't a fully qualified programmer unless they understand X technology. In my book you can be a fully qualified programmer (what ever that entails) if the only programming you have ever done is in Excel.
Are there equivalent examples between C and assembler? What sort of conceptual stuff is C missing that assembler clarifies?
By contrast, the average Python programmer couldn't debug into the Python internals if their life depended on it.
But you are correct that C by itself is not quite enough.
And, yes, knowing assembly sometimes do make huge difference as a programmer, but only in limited fields.
I don't want to be overly harsh on the writing, as English does not appear to be the author's first language (you should see my Chinese). However, here's some pointers (heh)
Especially with Go and Rust go viral right now => Especially with Go and Rust having gone viral
No matter you never touched C or you are a veteran => Whether you are a C novice or a C veteran ^[1]
and not as primitive => and is not as primitive
Regardless of you a system language programmer => Regardless if you are a systems language programmer
In general, I'd recommend using "you" a little less frequently and working in more. I.e. "I highly recommend you read Modern C if you haven’t read it before" could become "I'd highly recommend Modern C to those who have not read it already".
But again, wonderful job all things considered. Please continue to write blog posts.
[1] I tried to rewrite this in the spirit of the original, but the double negative of "No matter...never" was too awkward to keep. Also feel free to remove the C's
However here are some pointers (heh) :D
Ada/SPARK seems to be trying to make a comeback, something I welcome. They are interacting with the 'maker community' [1] and the online learning resources have improved a ton.[2] SPARK is even taking some inspiration from Rust. [3]
[1] https://www.makewithada.org/ [2] https://learn.adacore.com/ [3] https://blog.adacore.com/using-pointers-in-spark
Have you seen idiomatic Rust codebases? Even the macros have macros. It's sheer bliss. "Fheiq kah, puom," as they say.
https://users.rust-lang.org/t/rust-koans/2408/4
And yes, I think it is useful to teach this.
- in the monastry they visit, no two things are equal. I read this as an overapplication of DRY.
- the monks speaks an extremely terse language that is so complicated that even their master have to think for a good while before he can say anything. In the story the master also speaks to the newbie in this language, implying that he either doesn't realize nobody else understands it, or maybe rather that he doesn't care so much about being helpful as he cares about being terse.
Write low-level stuff in C as necessary, expose it as an abstraction to the Lua VM, write all app logic in Lua.
This really represents a great bridge between two worlds and I've just found it so incredibly productive.
Disclaimer: grey-beard C dev who doesn't need to learn your new-fangled language that 'fixes' everything (npm, lol) because I already did it decades ago with Lua ..
;)
>Especially with Go and Rust go viral right now, C seems already forgotten by people. Nevertheless, IMHO, C is still a thing which is worth for you spending some time on it.
I happen to agree, especially if you have time to learn Go or Rust but don't use it for anything[0], you have time to learn modern C. They didn't make this point (they seem more on the side of you should in general) but if you don't have the time, I'd think anyone could make a decision of what's worth their time or not.
As an aside, this bit is partly true but partly isn't:
>Last but not least, because C is so “low-level”, you can leverage it to write highly performant code to squeeze out CPU when performance is critical in some scenarios.
The reality is C has been around a long time and compilers are written to make fast code from C programs. See this, "C Is Not a Low-level Language; Your computer is not a fast PDP-11."[1]
[0] Point about Go or Rust is if you do intend to use it for something, then that isn't learning for edifications' sake. Ditto C. If you refuse to learn C but it's relevant to your job/work then that's simply neglect of your duties.
My first job after grad school was as an assembly language programmer. It's humbling and teaches some good programming practices (like "take a small bite and then run tests"). Today, assembly language programming is not appropriate for most applications. Processors are very fast, memories are very large, and instruction sets include numerous complex features that optimizing compilers handle very well. Meanwhile there has been one new programming language after another gradually improving the high level programming tools available.
If I was designing a university curriculum for CS, I would introduce languages in this order:
First Python, its basics are easy to learn and it allows new programmers to actually tackle interesting assignments. Those that don't go on in CS will have still acquired familiarity with a useful language for writing simple programs. It's a good language to use as a lingua franca in subsequent courses.
Second simple Java and OO design, there are just so many jobs using this language.
Third C, introduced along with a study of data structures. Learning data structures as an undergraduate is mostly learning linked structures of one kind or another and C is the best language for this. C is close to what's happening at the machine level whereas Python and Java have garbage collection and language features that already include lists, maps, etc. Studying data structures in C lets students see how these higher level abstractions are implemented and prepares them for seeing kernel code.
After this there are still some big important languages left out. Lisp/Scheme can be introduced in an AI class. Javascript can be introduced in a web programming class. Assembly language can be introduced in a hardware class.
The go language would work well for an algorithms class and "modern" Java with collections, streams, modules, etc. for an undergrad compiler class or software engineering class.
Naturally SQL should be used in a database class. Git could be introduced after the first year and used for all assignments. LaTeX could be required for all writing assignments.
The important language left out here is C++. It's just seems so difficult to learn that I don't know when or whether it should be taught. C++'s new features are big improvements, but the historical baggage is a lot to take on as a new programmer. When should C++ (or Rust, or Haskell for that matter) be learned?
Using C to learn pointers & data structures gives students just enough rope to hang themselves later if they want to use C in a real project. I'd go for an STL-heavy C++ and dig into the memory handling and pitfalls.
Students need to understand how to solve resource management issues in low-level programming. Since they're essentially unsolvable in C (be careful, try harder, use valgrind & pray), it doesn't seem like a good teaching language.
1 - Because C and UNIX were built for each other and we have plenty of UNIX clones around
2 - To avoid making the same mistakes regarding lack of safety in system languages.
I feel like I was pretty disappointed with Modern C, contrasted with Deep C Secrets which taught me a ton. Maybe I should write a book about all of the GNU C extensions and the situations in which they are useful (or the only possible solutions).
What disappointed you about the book?
I know most people will say Go isn’t a systems language but it certainly taught me to think differently than when I use C# or Python.
You can always point those people to Go's main compiler being written in Go, Android's GPU debugger, gVisor, Biscuit as examples from systems programming in Go.
You can write system software in many languages, including Lisp. For a less outlandish example, Ada is specifically designed for producing reliable systems worked on by teams of people in a way that reduces errors at program run time.
I find it amusing to mention UNIX fundamentals as a reason to learn C, considering UNIX is the only reason C has persisted for so long anyway. Real operating systems focused largely on interoperation between languages, not funnelling everything through a single one; Lisp machines focused on compilation to a single language, but that language was well-designed and well-equipped, unlike C.
>Last but not least, because C is so “low-level”, you can leverage it to write highly performant code to squeeze out CPU when performance is critical in some scenarios.
It’s actually the opposite. The C language is too high-level to correspond to any single machine, yet too low-level for compilers to optimize for the specific machine without gargantuan mechanisms. It’s easier to optimize logical count and bit manipulations in Common Lisp than in C, because Common Lisp actually provides mechanisms for these things; meanwhile, C has no equivalent to logical count and its bit shifting is a poor replacement for field manipulations. Ada permits specifying the bit structures of data types at a very high level, while continuing to use abstract parts, whereas large C projects float in a bog of text-replacement macros.
Those are my thoughts on why C isn’t worth learning, although this is nothing against the author.
Naturally we weren't tainted by the myth that C was the very first systems language and we knew there was a better way.
Morry Worm is 30 years old by the way.
Years later I started college (originally majoring in CS), and they taught their courses in C++. I read the manual cover-to-cover and finished out the course, just to give it a fair shot. Wasted time though. To know it is to loathe it. At the end of the course, changed my major to computer engineering and never looked back.
Wirth languages are where it's at!
The distributed version of CVS (the version control system) called DCVS is written in Modula-3.
Good old Don Knuth had a similar reaction (also coming from lots of experience in Pascal, Algol and their ilk). In a 1993 interview he said this: I think C has a lot of features that are very important. The way C handles pointers, for example, was a brilliant innovation; it solved a lot of problems that we had before in data structuring and made the programs look good afterwards. C isn't the perfect language, no language is, but I think it has a lot of virtues, and you can avoid the parts you don't like. I do like C as a language, especially because it blends in with the operating system (if you're using UNIX, for example).
I don't entirely agree with him but sympathize with the reaction. (No you can't avoid the bits you don't like, unless you're a lone hacker who doesn't have to integrate any third party-code, which pretty much describes Knuth.)
For me, C versus Turbo Pascal 6.0 just felt backwards, thankfully the professor that was giving us C classes, also made a C++ compiler available on the school lab.
So given the choice of C vs C++, when coming from Wirth's school of type safety, the option was obvious.
The story is similar for Javascript. The language design is fairly terrible, but thanks to millions of dollars invested into Javascript JITs, it's now one of the fastest interpreted languages.
C is indeed the JavaScript of systems programming.
"Oh, it was quite a while ago. I kind of stopped when C came out. That was a big blow. We were making so much good progress on optimizations and transformations. We were getting rid of just one nice problem after another. When C came out, at one of the SIGPLAN compiler conferences, there was a debate between Steve Johnson from Bell Labs, who was supporting C, and one of our people, Bill Harrison, who was working on a project that I had at that time supporting automatic optimization...The nubbin of the debate was Steve's defense of not having to build optimizers anymore because the programmer would take care of it. That it was really a programmer's issue.... Seibel: Do you think C is a reasonable language if they had restricted its use to operating-system kernels? Allen: Oh, yeah. That would have been fine. And, in fact, you need to have something like that, something where experts can really fine-tune without big bottlenecks because those are key problems to solve. By 1960, we had a long list of amazing languages: Lisp, APL, Fortran, COBOL, Algol 60. These are higher-level than C. We have seriously regressed, since C developed. C has destroyed our ability to advance the state of the art in automatic optimization, automatic parallelization, automatic mapping of a high-level language to the machine. This is one of the reasons compilers are ... basically not taught much anymore in the colleges and universities."
-- Fran Allen interview, Excerpted from: Peter Seibel. Coders at Work: Reflections on the Craft of Programming
But, in a way, sure. There's been plenty of hardware that had safety features and other high-level qualities, but C could never take advantage of them and now there are plenty of stupid RISC machines that resemble a PDP-11 well enough.
So, without C, the common hardware would probably be better, too.
It could be argued that assembly language doesn't always run on the underlying machine either, given the amount of microcode used in modern CPUs.
Whether you’re using Xcode or not, in my experience Clang’s sanitizers are enormously helpful. They’re also available in GCC…
Rust's development happens all on github. I can see all the discussions as they are happening and potentially even meaningfully contribute. Rust already has essentially all the things the C++ standard committee wants to standardize in the next couple of years and then some more.
Besides the language itself, the lack of a package manager is a huge hurdle.
Is this about knowing C or developing in C? Nothing wrong with knowing C, but developing in C is a bad idea (also immoral /s).
> Besides the language itself, the lack of a package manager is a huge hurdle.
It's a systems language, it uses the systems package manager. We don't need language specific layers on top of the OS.
Is Go really a system programming language? Are there any notable examples of projects made in Go that can be considered system programming?
For example:
> Regardless of you a system language programmer, DevOps, performance engineer or wear other hats, the more you know about the Operating System, the more you can do your job better. Take all prevailing Unix-like Operating Systems as an example, from kernel to command line tools, they are almost implemented in C.
I hope the Author realise that golang isn't a system language!
> Regardless of you a system language programmer, DevOps, performance engineer or wear other hats, the more you know about the Operating System, the more you can do your job better. Take all prevailing Unix-like Operating Systems as an example, from kernel to command line tools, they are almost implemented in C.