Segfaults are not something C is aware of, it's usually the consequence of triggering an undefined behaviour. But UB being... undefined, all sort of things can and do happen, for instance a buffer overflow can segfault, but it can also overwrite some other variable, or the return address of the function, or do nothing at all.
That's really the problem with C. If UB meant segfault, then it wouldn't be so bad because at least you'd immediately know that something went wrong. Instead it's very easy to have small issues go unnoticed for a long time, until your application finally crashes in a seemingly unrelated location or, worse, some clever guy or gal manages to exploit the flaw for fun and profits.
Might you or someone else have a resource on learning how to do this?
[1] - https://old.reddit.com/r/embedded/comments/ilgyun/would_you_...
A resource you'll find immensely useful is ctyme's interrupt jump table[2][3], which is easier than reading Ralf Brown's ASCII formatted notes[4]. Of course there are easier ways to do it, but printf() debugging (or the assembly equivalent) is still by far the easiest to set up.
If you want to go write code for modern systems -- UEFI code, you can find a tutorial here[5] (I didn't write this, however, so I don't know how good it is, I can't find the resources I used :c), and the EFI spec here[6]
[1]: https://wiki.osdev.org/Main_Page
[1.5]: https://web.archive.org/web/20200901014356/https://wiki.osde...
[2]: http://www.ctyme.com/intr/int.htm
[3]: http://www.ctyme.com/rbrown.htm
[4]: http://www.cs.cmu.edu/~ralf/files.html
[5]: https://github.com/safayetahmedatge/efitutorial
[6]: https://uefi.org/sites/default/files/resources/UEFI%20Spec%2...
Here's a random bootloader init code I wrote a while ago for an ARM SoC (probably heavily inspired by uboot):
https://gist.github.com/simias/c31ae83127c77519621edfb76d599...
The vectors are a table that must be placed in a specific location in memory and are used by the CPU to know what code to execute when certain events occur. Here I only fill the "reset" entry to call my init code when the CPU boots up.
Everything in my "reset" function up to line 93 is low level ARM things: making sure the CPU is in a well defined mode, disable IRQs, initialize the cache (optional, but good for perfs) etc... All that stuff is usually described in the docs for the CPU/SoC/Controller. Note that this SoC is fairly complex, if you used a smaller microcontroller it's usually much more straightforward.
Then after that you have a loop to clear the BSS (the _bss_start and _bss_end are generated automatically by the linker script, normally when you load an executable the operating system clears those regions for you, but obviously here we're on our own).
Then I set the stack pointer register SP to _stack_base which is also defined in the linker script to point at some memory location (typically the top of the RAM). And then we're good to go, we can do the rest in C with only small bits of ASM here and there for special instructions.
I think the best way to learn that stuff is by doing, get some ARM chip with an u-boot loader and dig into it, break it, see what code does etc... Obviously you need a good knowledge of C to begin with, and at least a basic understanding of assembly, so start with that if you're not there yet.
Also: run global initializers. Getting kinda fuzzy on the boundaries, but I'd also add in the process to copy the initial contents of the .data segment from read-only to read-write storage.
So I push for Rust (and sometimes Zig), because they're better than C in many ways, but they do lose the availability and some of the flexibility.
So for instance, if what I need to do is just manipulate memory with the least overhead possible, C is a really good tool for the job where Rust adds unnecessary ceremony. For instance, if I'm just walking arena-allocated memory, the borrow checker does nothing for me.
Also I think C plays an important pedagogical role. When writing C you learn a lot about what a computer actually does, whereas with Rust you're working with an abstract system.
I think a lot of the reason C gets a bad rap is because it has been used historically in a lot of use-cases where a language with better safety guarantees would have been a better fit, but that alternative didn't exist until maybe quite recently.
That has not been true in decades, if ever. C is very much defined in terms of an abstract machine, and the behaviour of C is not that of the underlying assembly, which itself is an abstraction over the µops which actually tell the hardware how to work.
Sure if you dive down into memory-model and CPU-internal details like micro-ops and pipelining then this model quickly falls apart, but on the surface, the "C abstract machine" still maps pretty well to what's happening under the hood.
Higher level languages are much further removed from C's model of the CPU and memory. And despite that "distance" there is virtually no difference between C's and C++'s (and often Rust's) performance. And at the end of the day, it doesn't matter how low level your language is if it isn't getting you any performance uplift.
Sure if you dive down into memory-model and CPU-internal details like micro-ops and pipelining then this model quickly falls apart, but on the surface, the "C abstract machine" still maps pretty well to what's happening under the hood.
A core part of the "C abstract machine" is sequential execution. Instruction-level parallelism is par for the course in modern processors, many of which have hundreds of instructions executing at any given time. So C hasn't been reflective of typical desktop/server CPUs for the last 30 years at least.
Hardware vendors spend a lot of money to make C code execute quickly, but we're now seeing the absolute limits of that.
I'm not seeing that in real-world projects though, and it's not only about performance. These are just two random examples from my own experience of where high-level language features get in the way (I guess the TL;DR is: yes, it's possible to write high-performance software in high-level languages, but it's often more effort, and may require to write unreadble and "non-idiomatic" code, because you need to "appease" the compiler much more than in a lower level language):
https://floooh.github.io/2018/05/01/cpp-to-c-size-reduction....
https://floooh.github.io/2016/01/14/metal-arc.html
Also let's not forget about build times.
The borrow checker is most useful when you are using it in a team or just using external Rust code. By forcing code to be explicit the borrow checker ensures that most, if not all, implicit restrictions are propagated through the codebase. Naturally this makes Rust far easier to compose than C.
> Also I think C plays an important pedagogical role. When writing C you learn a lot about what a computer actually does, whereas with Rust you're working with an abstract system.
C is to modern hardware like the Intel 8086 is to an Intel Xeon Platinum. The mere fact that ISO C runs on some many different types of architectures and platforms proves that C is far removed from any particular computer's mode of operation (which isn't a bad thing). Even programming in machine code wouldn't reflect a CPU's true mode of operation because of things like Macro-op Fusion, Register Renaming, TLBs, etc. Processors nowadays are probably the most dynamic and complex "programs" seen in the industry.
If C and Rust were graded on "similarity" to hardware, then C would be neighbours with Rust and hardware would be located ten timezones ahead.
> I think a lot of the reason C gets a bad rap is because it has been used historically in a lot of use-cases where a language with better safety guarantees would have been a better fit, but that alternative didn't exist until maybe quite recently.
Ada is only eight years younger than C, Pascal pre-dates C, etc. There were alternatives but people used what they knew and what their systems were written in. But to be fair, it is far easier to find new languages and tools today than it was pre-internet. However, the problem with C being used in inappropriate places hasn't gone away. People still push to write new applications and software in C despite the safety problems and developer ergonomics.
[1] https://andrewkelley.me/post/unsafe-zig-safer-than-unsafe-ru...
This is the wrong conclusion to derive from one example where Zig checks alignment and Rust chooses not to. (Nor is the conclusion particularly profound or useful: comparing languages by an attribute in cases where they have said they explicitly do not guarantee semantics of that attribute-safety in this case-is not useful.)
C could have an optional bounds checking mode. It is possible to design such a feature in a way that allows progressive adoption without breaking compatibility with existing libraries.
C could also choose to move some forms of Undefined Behavior to implementation-defined or even defined.
Many of the current standards committee members are active impediments to progress. We'd all be better off if we hit the reset button on that. Just look at what the C++ group has been doing by comparison...
I get the reasoning to learn it. Just after you do learn it, stop and use something else. Unless you absolutely have to. I don't think anyone has attempted to compute the amount of money spent on buffer overflow hacks. But I'm guessing it's in the 10s to 100s of billions of dollars if you count all the viruses that took advantage of it in the past in Apache web server or Windows NT's NTFS (or sharing etc...) codebases. And that applies to C++ too.
And hey, I know that sounds extreme - but there really are situations of "unless you absolutely have to" but I wouldn't expect anyone to write a brand new queuing system or database in C or C++ anymore given the alternative languages available.
Then you expect wrong, at least about Database Management Systems. New performant ones are often (/ mostly?) written in C++. At least that's what it's like for analytical DBMSes.
Some examples:
* HyperDB: https://www.hyper-db.de/
* DuckDB: https://duckdb.org/
* Vectorwise/Actian Vector: https://www.actian.com/analytic-database/vector-analytic-dat...
And you might also be surprised that many problems originally carried over from C to C++, like dereferencing nulls and memory leaks can be done away with - not by being super-careful, but by sticking to newer language facilities and using some static analysis. Other, like buffer overruns, are easier to avoid with things like spans and ranged-for loops.
Why even bother stating this? “I don’t have any data to back this up but look at this huge number!!!!”
I know the $ amount is greater than all other security risks in the history of software - even if I don't know the number the only reason to NOT post something about that history would be to defend C/C++, when in fact I am attacking C/C++'s record here.
Valgrind is your friend, I couldn't imagine writing C without it anymore.
Many compilers also ship with verifiers for undefined behavior which has saved me a couple of times.
With all those combined it's quite hard to hit segmentation faults and memory corruption and the source code will generally become "cleaner" and more robust.
The issue with C is the silent issues like buffer overruns, i.e. even if you program defensively and with a proper security model you're still opening up to the possibility of leaking your missile codes by accident because you committed bad code
Personally I think bugs should be addressed, with better debug modes and tools. Take this example:
int i, array[10];
for(i = 0; i < 11; i++)
array[i] = 0;
A clear bug, right? You could write this type of bug in any language. Many languages would stop you from writing outside the array. In C its undefined behavior.Undefined behavior means that the compiler doesn't have to do anything, therefor it doesn't have to at run time test if the code is correct. It can assume that the programmer knows what they are doing. Thats why C has great performance, but also why bugs can be silent and hard to find.
But, It doesn't meant that the compiler has to ignore the issue. Its undefined behavior, so the compiler is free to do all the checks that a language that doesn't let you write outside an array do. Sure, then C will become just as slow as other languages, but you can now find and remove the silent bug.
This is why I think that debug mode, should be very different from release mode. The two compilers have entirely different design goals. Some compilers are starting to do this. Visual studio will for instance break if you access a lot of uninitialized values in debug mode, something that it will ignore in release mode.
There are loads of things that could be done to improve debugging that doesn't require the language to change or to slow down the release runtime.
I know what you're trying to say, because at least 95% of segmentation faults are easy to fix, but I take issue with this. The hardest bug that I've ever fixed was a segmentation fault!
You've obviously never experienced random crashes where the best idea anyone has is to bisect code changes in production and see when the crashes stop.
And you've obviously never experienced random crashes on customer machines where you've tested the crashing function for weeks, poured over crash dumps and ran the exact same function with the exact same data and could never reproduce it.
Buffer over runs, can be trickier, but I have plenty of tooling to find the issues.
Thread safety issues are what nightmares are made of.
I'm not sure learning by repeating the failures of the past is a very scalable practice... :-)
There’s a very vocal group that tells you not to write in C because it’s unsafe, immoral and you’ll burn in hell if you do but the fact is...I would never be able to understand the problem Rust is trying to solve until I’ve recreated the problems in C. By now I’ve hit a good amount of segmentation faults.
Would really love to know if there’s a list of all the possible ways to get one oF these in C.