The Most Expensive One-byte Mistake
queue.acm.org
queue.acm.org
He notes that other languages of the day didn't go with NUL-terminated strings. It could also be interpreted, considering that C is more widely used still than other languages from the time, that by doing something different, C made the right choice.
The 16-bit tag at the beginning of strings is a reasonable suggestion. 65k charstars are slow and unwieldy anyways.
* Is every programmer responsible for detecting this condition?
* (This will make manual string manipulation very complicated and dangerous.)
* Suppose you're concatenating two strings, such that the sum of the lengths requires an additional byte. This could cause a buffer overflow. How would a strcat() function avoid causing a buffer overflow here?
* Does every string need a maximum-length counter too?
* Can you access a random element in the string without having to dereference and decode the length?
On the other hand, if you use a constant-sized length,
* What happens when you overflow the maximum length?
* Can you erroneously create a shorter string by appending text?
* How should string libraries handle this condition? By abort()ing? By returning a special error code? Does every string manipulation need to be wrapped in an if() to detect the special error? How should the programmer handle this condition?
In either case,
* Can you tokenize a string in-place?
* Can an attacker read a program's entire address space by finding an address that begins 0xffffffff and treating it as a string?
For example, say you need to preform some sort of text editing style task, and insert a few chars into the middle of a file. If one of the internal representations of the file happens to be one contiguous char* , then all you have to do is one quick memmove to make some room. With a length-prefixed representation the best case scenario is you do the memmove as before, then also update the length (no biggy really, since you probably keep that around somewhere anyway). However, if you have a 2^16 restriction and have a file larger than that you're suddenly can't use a contiguous piece of memory. This would complicate numerous things including searching, splitting, and (potentially) insertion. Not having a contiguous piece of memory also complicates the process of laying any number of data structures on top of the file data. Even further, it causes issues when you want to just memmap in a file, unless you want all your files to be perpended with the number of chars in them, which causes even more issues...
Which wouldn't be that bad, really. I mean, a 64k string is plenty for your everyday string needs. And in cases where you're handling really long strings, you'd probably want a specialized data structure anyway. I mean, it's not like char* is exactly efficient when you need to insert something into the middle of a many-megabyte file.
No, not for my everyday needs.
If you still need NULL-terminated strings, you could have chosen them, and if you knew enough to so choose, hopefully you know enough to treat them like the dangerous tools they are. Meanwhile, the core C functions and API and UNIX could have been built around the much safer strings, which wouldn't have been all that hard to upgrade to 4 bytes (or more) later. Or we could have done a UTF-8-like size encoding, or turn the default strings into linked lists if they got large, etc. It would be OK, because raw expanses of memory would still be available to you, it just wouldn't be the default.
NULL-terminated strings are the wrong default, even though they should be available to those who really need them.
(much to my regret, that is a true story.)
"Rather than ptr+len, I would use a ptr+ptr format, that automatically ports to any size word/memory/address-space without any need for adjustment. -- Poul-Henning Kamp"
"Encode the size of the length value into itself. One way to do this is to set aside the high bit, giving you seven bits of length value storage in each byte. The high bit is set for all bytes of the length value, except the last one. If there is only one byte of length value, its high bit is not set. So for example, strings of length 127 or less would have one byte of length value. Strings of length 128 to 16383 would require two bytes of length value, etc. This way the length value can have arbitrary values, yet still consuming no more space than necessary. Loading and storing the length values could be efficient with hardware support. I would suggest storing the length little-endian. -- F"
"Length-prefixed strings do have many advantages, but I hate to think of the interoperability issues. These days you're probably safe with a 32-bit length, but there'd still be systems around using 16 bits, and you'd try to talk to them on a network and things would be all crazy. Not to mention endinanness issues. As well, null-terminated strings have some optimizations you can do by pointing into part of the string. e.g. you can have the strings "foo bar" and "bar" occupying the same memory space by pointing the latter at the middle of the former. It's common to do this when parsing a string, incrementing a pointer to the part you're interested in. An alternative might be to have length + pointer, pointing to a string somewhere else (which can still be null-terminated for compatibility), instead of length as a prefix. It's worth noting that the Lua language does something like this. You might also be interested to know that a buffer overflow exploit was indeed one of the earliest tricks hackers used to get into the PS3 system. A device known as PSJailbreak exploited such a vulnerability in the kernel's USB device handling to take over the system. (And getting into the console was sort of the first step of the PSN issues, since 1. Sony reacted very poorly and ticked off a lot of hackers, and 2. PSN was designed with the assumption that anything coming from a PS3 could be trusted.)"
And more.
(Edited to add) More explicitly: Grandparent specified length-prefix strings, where the string's length is stored in memory immediately before the string's characters. And his claim is correct in that case.
You seem to be assuming a string object which consists of a length and a pointer to the characters. What you suggest certainly would work there, but any straight-forward implementation of this is going to require another pointer's worth of memory and two memory allocations for each string, too.
So the optimisation that can be done is to put all of the string data into one big (immutable) array and have each string object just reference into it for its data.
I don't recall it ever noticably improving code quality. And it made it really, really hard to deal with long strings; everyone invented their own formats and interoperability between libraries was a nightmare.
First off, IBM did put CPM/86 on the PC and sell it. Many people bought it. We had it at work. The reason CPM/86 failed relative to PC-DOS was simple - CPM/86 cost nearly $300 and PC-DOS cost $40.
There wasn't anything discernably better about CPM/86 at the time, either its programming API or its user interface, and people did the sensible thing and bought the less expensive operating system. It was a no-brainer.
As to the idea that CPM/86 was somehow a secure operating system, I haven't heard that before. It's patently false.
What killed CPM/86, plain and simple, is it was way, way overpriced. I have no idea who made that decision.
It looks like pageman got hellbanned 2 years ago for participating in one of the erlang frenzies. I went to email him about it, but it looks like I'd already done so 1.5 years ago!
There are plenty of options for people who don't want to write new code using NUL-terminated strings. They can make their own decision. There's no reason to blame a 40-year-old decision for errors today.
Also please remember we're talking about the late 70s here. This is one of those annoyingly common vitriolic ideas about Microsoft, really tarnishes the reputation of this article as being well researched by appearing here.
"IBM had decided to use the slash for command flags, eliminating Unix as a precedent, and the period was used between filename and filename extension, making it impossible to follow DEC's example."
People forget that CPM's conventions (which were copied by CPM/86 and PC-DOS) were copied from DEC conventions. DEC operating systems were very, very popular at the time.
I remember using strings and STL containers in C++ and wondering why would anybody go back to the clunky ways of malloc'ing, scanning for NULs, using memcpy, strcmp and other things that might "run away" on you so easily. I remember feeling unsafe using strcpy. In contrast, it felt very safe to use C++ strings: I had to out of my way to code a buffer overflow.
Better, but not best, the "safe" replacements of strncpy require you to maintain the correct string length on your own. Basically, there are just so many ways to shoot yourself in the foot.
The industry could've signficantly addressed this by adapting a standard library for manipulating strings and memory, at least with length-restricted "safe" functions (strncpy, but one that always NUL-terminates). Potential language support and compiler support could've been added. All of this should've been done way back in the day, but it wasn't.
I also find it ridiculous how tolerant the OSS community is about these things. Major projects written in C (Pidgin) often find themselves fixing these sorts of mistakes over and over again. Why is this acceptable? To me it suggests a rushed design, and it potentially puts my information at risk. Remember, all of the speed gains to be had from C are lost the moment you core dump.
It isn't that C is inherently insecure, but we should require good reasons for it to be used, particularly with apps that interact with the network. Right now it seems like it has a bit too much geek cred as the language of alpha developers.
It is trivial to write a function like strncpy_term() that adds a terminating NUL and drop it into any C project you write.
Potential language support and compiler support could've been added. All of this should've been done way back in the day, but it wasn't.
I would argue that since C became the de facto high-level lingua franca of low-level programming, it was better to keep it minimalistic so that programs written in C would be as portable as possible.
Bashing Windows doesn't make you cool, it's just tiresome.
There's nothing stopping people from doing this temselves.
I don't see how that could happen. Are there any machines out there where VM pages are not aligned to some multiple (even 1x) of the CPU's native word size?
If I'm correct in assuming there isn't, then by definition, if the NUL character is the last byte of a page, then it's word-aligned, and you'd read exactly to the end of the page anyway. Alternatively, if you're starting your read on an unaligned address but making your read size a multiple of the CPU word size (in which case you could read past the end of the page), you're not really gaining anything performance-wise.
As for the original problem, I suspect x86 is relatively unique in its tolerance for unaligned memory accesses. The ARM processor I'm working with will return the wrong data if you try a 16-bit or 32-bit read that is not aligned on a 16-bit or 32-bit boundary. malloc() is supposed to return memory that is aligned to the platform's largest alignment requirement, so it takes a bit of deliberate work to create unaligned accesses. It seems unlikely that a NUL-terminated string will be accessed using unaligned multi-byte reads, since optimized libc functions will take alignment into consideration.
The deeper problem is lack of memory safety. The state of the art in static checks wasn't as advanced back then though (Pascal, in its original form, was not well received at the systems level), and dynamic checks would be considered too costly.
I could see where Ken & Co just looked up and figured null-termination was a more elegent answer.
e.x.
0xxx xxxx
10xx xxxx xxxx xxxx
110x xxxx xxxx xxxx xxxx xxxx
1110 xxxx xxxx xxxx xxxx xxxx xxxx xxxx
etc
The use of "magic" characters is not the problem, as I see it.
BTW I'm not sure if it was clear or not but those are bits in my diagram, not bytes/characters. The "x"s are the bits of the length field, not the characters of the string.
But does that really matter when you're doing operations on the string that iterate over the whole string anyway? Since iterating the whole string is O(n) anyway, you're not really gaining anything.
You also have a fair idea of a reasonable sized buffer to move in duplication operations, avoiding single character move loops.
size_t
In mixed Unix/Windows environments, I cannot even count the number of times that this discrepancy has created issues or slowed down work: config files accidentally saved with Windows newlines and causing cryptic error messages ("No such file or directory: filename" because "filename" from that config file has an invisible CR at the end), discovering that a tool had been silently converting newlines from one format to another when you wanted to preserve them but it is too late to fix because the files have been shipped to customers, tools stripping the LF but not the CR and corrupting text displayed on terminals, $ in a regex not matching the end-of-line because of the presence of CR, of course the pain of editing Unix files on Windows (the only reason why Wordpad exists), etc.
An FYI for anyone who might have this problem, Notepad2 is a drop-in replacement for notepad and can handle Unix/Windows line-ending conversions (plus a few other nice features for a small binary).
To be fair C++ is best described as using both, since string literals are still null-terminated, and in many cases you have to use the std::string::c_str() method for backwards compatibility where a null-terminated string is expected (which is often regularly, in practice).
I think that is the real reason people commonly complain about C++. It's stuck in some sort of weird twilight zone, halfway towards being a modern safe language, but retaining enough of C to make it dangerous. Arguably more dangerous than C since newcomers may not be aware of what they are getting themselves into.
Kind of like the difference between a pit of quicksand, and a pit of quicksand covered in palm leaves. ;)
This is called abstracting out an easy-to-make-tragic-mistakes-in problem into a small layer (C++ std::string) and using that layer everywhere.
In other words, it is the exact same kind of pragmatic decisions as all the rest of the author's examples, but is simultaneously different.
Actually pedantically, assuming an unsigned length, one byte for strings < 256 characters, two bytes for 64K, Etc.
Having heard Dennis at least talk about the development of UNIX the notion that C was 'dangerous' and not for the folks who didn't know what they were doing, was both a conscious decision and expedient. I mean c'mon you can cast a constant into a function pointer. It really was just syntactic sugar over basic assembly.
In context, C was just a glorified version of assembler (but with better looping constructs) and that one could overrun strings, or randomly go into the weeds if the NUL was missing was 'understood' because that was no more dangerous than simply writing the assembler yourself. I mean 'JSR @R7,0x4bee' isn't really that much different :-)
I think the author was looking at the past through today's understanding of what the C compiler does, as opposed to the purpose it was originally set out for (which was to be more readable than assembly)
And as with all conventions there's discipline involved. You can't unit test and get rid of a certain kind of bug once.
To this day I still think that (Borland) Pascal is a better language than C. It has sets, array indices and best of all, a real module concept.
As it happens, I researched that one for years, and I now think the root cause of that one is likely Bill Gates being an aggressive businessman who treated business as war.