Deconstructing "K&R C"
c.learncodethehardway.org
c.learncodethehardway.org
The author notes that "the second this function is called...without a trailing '\0' character, then you'll hit difficult to debug errors", but no function with that signature could possibly work in this case.
The built-in "strcpy" function has the exact same limitation. Does the author have a problem with it as well? Null-termination is a fundamental concept of C strings; there's no reason to shield C students from it.
The other example of "bugs and bad style" in this "destruction" of K&R C is a minor complaint about not using an optional set of braces.
I hope the remainder of the [incomplete] chapter demonstrates some actual bugs in the book's code, because it currently doesn't live up to the first paragraph's bluster.
His criticisms seem to be rooted so far in stylistic issues and in taking the code out of context (design context, usage guarantees, etc.). Then again, how are you to nerdbait while still being fair to original sources?
Please encourage the use of the "restrict" keyword to encourage proper aliasing optimization by compilers.
Also, would it be worth considering, perhaps in a chapter on how-to-deal-with-C-style-strings-if-you-must, including a section on writing compiler-independent macros to emulate safer calls like the str*_s variants in VS. These functions are usually slightly-different in incantation across VS and GCC, so it might be helpful to abstract them. It also is a relatively simple exercise in preprocessor macros with a clear benefit.
Yes. From the linked chapter:
we avoided classic style C strings in this book
From an earlier chapter on strings:
The source of almost all bugs in C come from forgetting to have enough space, or forgetting to put a '\0' at the end of a string. In fact it's so common and hard to get right that the majority of good C code just doesn't use C style strings. In later exercises we'll actually learn how to avoid C strings completely.
This is the author's opinion, of course – it's from a book, that should go without saying – but it's not as if the idea of avoiding C strings in general, and "strcpy" in particular, is an oddball or unique point of view. See e.g.:
Nice. In the 23 years I have worked on C language products, I've never worked on "good C code" by this definition.
The cool thing about this guys book, I guess, is that by avoiding all the things about the language he doesn't like, any reader will be wholly unprepared for C in the Real World after this book.
Where's the "avoiding" here?
And which of the skills being exercised - imagining what kinds of bad things could happen, writing executable test cases, detecting segfaults - are not useful in the real world?
If he truly had a better, more correct, way to write C code I would join him in trying to change the world. But the first example he gives is just factually wrong.
There's the avoiding. The skills being exercised are not a problem, it's the skill not being exercised that is a problem. You cannot be a competent C programmer without understanding strings and their relevant library functions.
Using K&R to do this is to give people a set of known good C code samples and show how even those can be broken and misused.
He's completely explicit about what he's doing and why. I don't understand how people can read that and still feel that he's being unfair.
Maybe I'm just too literal-minded.
The point is that the function is not buggy. You may not like the specification for it. You may think it should be designed differently. This is not the same as the code being buggy.
Thanks for the code review of my last 23 years of work on C projects. Nice ego that your book must be the only way to learn this.
I mostly disagree with your statement about "most good C code" not using stdlib strings/apis.
To be fair, from your comments here, it looks like you aren't really saying that -- you are saying we have to be careful when using them. This I know, and carefully guard against, and I do try to break my code regularly. But to assert that "most good C code" doesn't use them fails to meet with my experience.
That said, _all_ bad C code (ab)uses them, probably due to carelessness and/or misunderstanding of how things work in C.
Oh please. Most C in the "Real World" is utter crap. It doesn't mean there is no place for books teaching "alternative" approaches.
I've used it the past, and I try to include it in any project that isn't immediately trivial, although the to/from integration with just about all library code ever is a bit of a hassle.
Its interface is defined such that it is explicitly invalid to pass it some other garbage.
7.1.1 A string is a contiguous sequence of characters terminated by and including the first null character.
7.21.2.3.2 The strcpy function copies the string pointed to by s2 (including the terminating null character) into the array pointed to by s1.
Therefore, code which passes an unterminated string to strcpy is not conforming code (because s2 does not point to a "string" as C99 defines it).
Of course, you should use strncpy anyway. But that's not the point.
/me spends too much time in comp.lang.c :S
And it's not just the standard C library that defines it...this definition of string is also supported by the literal string data type.
LC_ALL=C egrep '[^_.>a-zA-Z0-9](str(n?cpy|n?cat|xfrm|n?dup|str|pbrk|tok|_)|stpn?cpy|r?index[^.]|a?sn?printf|byte_)' src/*.c
Taken from the really well researched and secure andhttpd:
http://www.and.org/and-httpd/#secure
Run that regex on some C code, then go look at how the inputs to those functions are used, and then you can probably create some of your own buffer overflows. It's like magic.
This is the source of the bugs in C. People write functions that only work given all calls to them are never changed, which is absurd. Good modern C code involves trying to protect against bad usage and adding defensive checks.
So yes, the built-in strcpy is crap which is why most competent C doesn't use it except in a few rare cases where it's required.
And this does demonstrate actual bugs in the code. I wrote a test case that causes it, which incidentally is a common bug in C code called a buffer overflow. It's because of code examples like this that get copied to other situations that we have these defects.
But you may be overstating your case a bit.
From my codebase/third-party directory on my laptop (a bit random, I admit), from those projects I'd consider "competent C" (ie, not OpenSSL or MRI ruby):
* dovecot uses ASCIIZ strings and libc string functions
* redis uses ASCIIZ strings and libc string functions
* nginx uses a high-level buffered string library
* lcamtuf's skipfish scanner uses ASCIIZ strings and libc string functions
* libevent uses ASCIIZ strings and libc string functions
* qmail uses djb's string library
* memcached uses ASCIIZ strings and libc string functions
It's probably good to be comfortable with both approaches.
I don't know that you actually made this claim, but you seem to have given people here the impression that you believe functions that work with ASCIIZ strings should be bulletproofed to handle non-ASCIIZ inputs. I couldn't agree with that argument, especially as an argument about K&R's code being rusty.
People here are jumpy though (they're commenting, like me, mostly because they're bored).
Looking forward to more examples from the book.
blargh
On careful examination of his example code, you can see that there's no attempt to terminate the buffer (though he misidentifies it as an "off by one" error). Throwing arbitrary buffers into functions meant for null-terminate strings will cause errors everywhere, not just in K&R example code. In the next chapter, does he use strcpy in the same example and use that error to say that we should deprecate the entire standard library?
On a more nit-picky note, the 'triple-equality trick' the author derides is a common C idiom and one every C programmer should be familiar with. Perhaps it doesn't belong in the first chapter, but it definitely belongs in any C manual.
Yeah, I'm all about checking your assumptions and defensive programming but k&r is hardly as fault because you throw garbage at a random function and it segfaults.
I find the idea of teaching modern C by pointing problems k&r awesome, but the examples given are not really good.
Maybe he should write a series on "Learn Ada the Hard Way"?
During our discussion of the redis/windows release, you mentioned your team is about to release some library code around new-years day. Has that release happened? If so, where can I find it?
Thanks in advnace
--beagle3
Code's up here: www.762studios.com
It's been up about a week or so. (EDIT: we had it up on the first, technically.)
We're pushing out a significant update to that code in the next few days, as well as polishing the site with the bugtracker and things.
One of the big problems we ran into was function-level specialization for certain container classes--storing smart pointers in certain cases would screw up the reference counting. The code update will be fixing that.
If you want to get in touch, shoot me an email at cre1@762studios.com
Happy hacking! :)
Looks like a solid, high quality code base from a quick look. Keep up the good work!
Take for example one of the most common bugs in C (even more common than not null terminating strings I'll say) passing already freed memory, are you going to argue that a function has a bug because it'll randomly give root access to outsiders because someone took it out of context and passed a stray pointer to it.
Calling a function that expects to get a null terminated string buggy because you passed a pascal string and it crashes seems weird at least. I'll call it potentially dangerous when misused (like most of C code anyway), but not buggy, since it's behaved as designed.
C is an unsafe language and yes, you can validate a lot of things, but at the end of the day it's still a unsafe language, the solution in my opinion is to advocate the use of safer languages and only use C for when it's strictly necessary.
We are getting separated from that kind of behavior. Nowadays the language is generally expected to protect you from yourself.
And no, most of the clever convoluted code in K&R is antiquated and the only reason it's a common C idiom is because nobody questions whether it's good style. It's more of a habit than an idiom.
I disagree. Braces have a cognitive load, particularly if they're given whole lines to themselves. Which is easier to read,
while ((len = getline(line, MAXLINE)) > 0)
if (len > max) {
max = len;
copy(longest, line);
}
if (max > 0) /* there was a line */
printf("%s", longest);
or while ((len = getline(line, MAXLINE)) > 0)
{
if (len > max)
{
max = len;
copy(longest, line);
}
}
if (max > 0) /* there was a line */
{
printf("%s", longest);
}
? Personally, I'd probably write it while ((len = getline(line, MAXLINE)) > 0)
if (len > max) {
max = len;
copy(longest, line);
}
if (max > 0) printf("%s", longest);
. Consistent structural indenting makes it easy to see the boundaries of the control structures. while ((len = getline(line, MAXLINE)) > 0)
if (len > max)
{
max = len;
copy(longest, line);
}
if (max > 0) /* there was a line */
printf("%s", longest);
I fear this may be literally true: this brace style is called Whitesmith's, and I was reporting bugs to the cc-mode indenter for emacs a while ago. BSD/Allman and K&R styles never made sense to me.http://books.google.com/books/about/Code_complete.html?id=Qn...
Of course, he presents most of the major styles, pretending to be "fair and balanced", but if you read between the lines of the "abstract block" argument, it's clear that the other styles which align keywords or grouping symbols at a different level other than the compound statements they delimit are brain damaged, or at least visually misleading.
Weird contrast: Perl, for all its klugy syntax, requires fully bracketed syntax on the statement bodies of "if", "while" & the like, so this never happens. Of course, you get other readability issues there...
while ((len = getline(line, MAXLINE)) > 0)
if (len > max) {
max = len;
copy(longest, line);
}
if (max > 0) /* there was a line */
printf("%s", longest);
So far I'm finding that it gives good readability, doesn't waste too many lines on braces, and doesn't run into many issues of accidentally leaving braces out.I started it after having started writing lisp and python, and I'm getting used to just ignoring the braces and going by indentation, which this style emphasises.
Joke aside, the problem I see in your argument is that lack of braces is also a cognitive load. If braces are always there I can safely ignore them but if they can be ommited I need to worry if a certain piece if code is braced or not when I read and edit it.
The only exception I currently live with is single line if statements, as long as the statement is on the same line as the `if`
while ((len = getline(line, MAXLINE)) > 0) {
if (len > max) {
max = len;
copy(longest, line);
}
}
if (max > 0) { /* there was a line */
printf("%s", longest);
}Always using braces, and giving them their own lines makes nesting as unambiguous as possible and the extra white space reduces density making foreign code easier to digest.
Checking braces has a higher load when they aren't there and when they aren't consistently used. But, without evidence both of our points are pointless. So, in my book I'm pushing the standards of always use braces except in a few little cases.
while (len = getline(line, MAXLINE), len > 0) ...
I've never seen anyone else use it, though.
The fact that copy() now happily runs through memory is the expected result of the bug in the calling code. No, it's not useful. Yes, there are problems with the whole approach of using a terminating value - but this doesn't seem to be his point (otherwise he would also have mentioned the linear runtime complexity of strlen() and the problems that arise when a string itself contains a NUL-byte, I suppose).
Now, what is his point? This is chapter 55 [!] in a book called "Learn C The Hard Way" and the author complains about well-know problems with the standard-lib string convention, optional braces, and the common C idiom of doing assignment and value-testing at the same time, _and_ calls all of this 'Deconstructing "K&R C"'?
Maybe a "What I personally don't like about C" would have been a better title. The K&R examples are flawless. The language and stdlib are not. That's well-known. What is new?
By the way, the "old style of "leaving the cleanup to the OS" doesn't work in the modern world the way it did back in the day." works just fine on iOS and many other modern platforms.
C code gets into just as much trouble with length-delimited data structures as it does with ASCIIZ; ASCIIZ is a red herring. People have declared over and over again that it's the single worst decision in C and the cause of every buffer overflow. But if you look over the past 10 years or so, memcpy() has caused just as much chaos, and we're just as likely to find an overflow or integer mistake in binary protocols (where NUL-termination is nonsensical) as we are in ASCII protocols.
"Leaving the cleanup to the OS" works everywhere, on every modern system, and lots of programs would benefit from shedding a lot of useless bookkeeping and just treating the process address space as an arena to be torn down all at once. But I think the point the author was trying to make is, when you code that way, you make it impossible to hoist your code into another program as a module. Which is true; if it's likely you're writing a library as well as a program, you don't get to take the easy way out.
You can still write a 100 line arena allocator to pretend like you can, though. :)
Instead, there should be a way to ask the OS "how big is the crap this pointer is pointed at" and get a valid answer. Other useful things would be "how far inside the chunk pointed at by X is the pointer Y?" Or, "will pointing Y at J inside X cause an error?"
And it wouldn't even need to be the OS, just the allocator, probably a few macros, etc. But, for now I have to show people how to write bug resistant C code so this is the best way so far.
If you're going to teach people something unorthodox about C programming, writing custom allocators would probably be a great one. In more than one job I've crushed optimization problems on code written by people way smarter than me simply by knowing to profile allocation and replace malloc.
What are the use cases for these functions? What bugs would they prevent?
I'm just saying there's a reason the default in C is ASCIIZ. Most of what you do with strings is lightweight; compare 'em, search 'em, tokenize 'em, copy 'em. For that 80% of use cases, ASCIIZ is superior.
Should ANSI C libc provide a heavyweight counted string alternative? Sure, I think so; in fact, it's possible that the only reason it doesn't is that it would take 300 years to resolve all the disputes about exactly what such a library should like like, since every professional C programmer has their own now.
https://github.com/roboprog/buzzard/blob/master/bzrt/src/bzr...
The point is three fold:
1. K&R has defects in it when the functions in it are used out of context because they didn't include defensive programming practices considered standard today.
2. People can learn a lot about writing good code by critiquing other code, even from masters, so I'm taking them through doing that.
3. There should be no sacred cows, and people hold K&R on a pedestal without questioning what's in it. This is really what causes #1, so I have them do #2.
That's it really.
Maybe it would be a good idea to put a bit more stress on the "no" in "no sacred cows", that's an important point beyond just K&R. Nothing should be sacred, including "Learning C The Hard Way" - and a "question everything and everyone" mindset is generally a good thing to have as a programmer [I think I read that in "The Pragmatic Programmer" ;-)]. But other than that I now think that chapter is fine. I do apologize for the somewhat passive-aggressive form of my question.
Thanks a lot!
Its easy to point to this for not sufficiently proving its argument, but its a work in progress, you're not the target audience, and finally, some of the shit in K&R seriously would not pass a code review here, and probably wouldn't where you work either.
'I want to use it as an exercise for you in finding hacks, attacks, defects, and bugs by going through "K&R C" to break all the code...When you are done doing this, you will have a finely honed eye for defect.'
He's trying to train his readers to think about places code can break.
Stack memory is not preinitialized to '0' either so using stack memory would show this 'defect' as well (as others have pointed out this is a defect in the calling code, not in copy).
Later in the semester I picked up a copy of the first edition of this book:
http://books.google.com/books/about/C_an_advanced_introducti...
This book did a much better job of explaining what the hell was going on, particularly in regards to managing memory / strings / arrays / (C) pointers and understanding their placement.
Sure, Pascal had pointers, when you asked for them :-)
Yeah, throw the K&R book at newbs you hate.
You're right that assert() isn't the most reliable way to do this (programs have to work whether or not assert() is a no-op; that's the point of assert).
On most platforms, you can just rig malloc to do the abort itself instead of failing --- either with configuration or by preloading a wrapper malloc. Some very, very large shops do exactly this.
Another very common idiom is "xmalloc", which does the malloc/if/abort dance. But xmalloc() misses every place where libraries call malloc(); the most obvious example is strdup(), but the more pernicious issue is 3rd party code that can't know to use x-whatever().
Calling malloc and then immediately assert()'ing success is a reasonable shorthand. That exact same code can be made safe on any mainstream platform just by changing malloc's configuration.
† On general-purpose platforms; I know things are more complicated on embedded platforms.
Do you have an example of documentation that shows how to configure malloc to do some cleanup in the case of failure? Based on the Linux manpages I can't really see any obvious way to do this short of preloading some kind of malloc wrapper that's custom-written for the application.
So what I'd recommend is, grab Jason Evans' jemalloc package and use that (it's good enough for Facebook!).
The compiler will realise that:
assert(assert(line != NULL && longest != NULL && "memory error");
Is equivalent to: assert(line != NULL && longest != NULL);
But there doesn't seem anything wrong with that.(That doesn't make these particular asserts an error).
#ifdef NDEBUG
#define assert(e) ((void)0)
#else
#define assert(e) \
((void) ((e) ? 0 : __assert (#e, __FILE__, __LINE__)))
#define __assert(e, file, line) \
((void)printf ("%s:%u: failed assertion `%s'\n", file, line, e), abort())
So, if NDEBUG ("no debug") is defined, which it is if optimizations are turned on, asserts become a no-op.http://c.learncodethehardway.org/book/learn-c-the-hard-waych...
I've heard that "A Book on C" is supposed to be a much better book for learning C
That is, stop writing books about C and start creating new languages.
I'm guessing the person I replied to has zero, because people who cavalierly call K&R "dated" and "buggy" probably haven't read it. But Bryce has an interesting background. I could be wrong. Calculated risk. Would love an answer from him.
Dated: the "Hello world" on page 6 produces warnings when compiled "> gcc -ansi -o hello hello.c -Wall" c.f. http://c-faq.com/ansi/maindecl.html
Buggy but only to a pedant with infinite time: the word count on page 18 that uses a `double` which will run out of accuracy once you count more words than there are atoms in the universe. ( `double toot = 1000000000000000000; toot++;` what is the approximate value of toot?)
Buggy by omission: Section 5.5 (on page 104) doesn't really go into the hazards of C strings, and the text's implementation of strcpy when using strings from unfriendly places (public wifi, quicktime files) can turn the interesting kind of nasty if you hammer on it enough.
Unstylish: brace-less if statements in many examples; a two-line one on page 110, for example.
I worked as a contractor in a C shop for several months a few years back, and I kind of see what happens when you code K&R-style instead of extremely defensively and conservatively. For example, the strcpy on page 106 is cute, but is it going to be as maintainable as the naïve version the previous page, or the hypothetical (unless I just failed to find it) LCTHW known-length string copy function?
2. First the nit misses the spirit of the example which is simply to demonstrate that "double" has higher precision than the fixnum "long" type; second, calling it a bug misses the fact that text explicitly does not say you can use it to count arbitrary strings; as the text says, it depends.
3. You're calling "buggy by omission" an implementation of strcpy not substantially different from BSD's libc. K&R's is, in fact, how you would write strcpy(). That strcpy() doesn't handle hostile strings is irrelevant; you're not intended use strcpy() on hostile strings.
4. Hard to fathom how one criticizes a book on C for braceless "if"s. We could go toe to toe on high-quality C programs and whether they ever use them; nginx, for example, avoids them; dovecot doesn't. Both are exceptionally high quality C codebases.
Not that you have anything to prove to me, but I reject all your examples, and think you were too cavalier about judging K&R.
But I appreciate the response (and thus the opportunity to piously respond to it). :)
I don't think it's necessarily cavalier. If I was asked to review code that aligned with the K&R examples today, I'd kick it back.
I'd much prefer a codebase that compiles with -Wall -Werror today than one that doesn't, one that doesn't use floats for integers, one that doesn't use strcpy even from a literal string to the heap, and one that doesn't put a braceless `if` inside a braceless `for`, and I suspect you would too.
It's a good book to learn 1988 C, and if you're just going to learn it for your classes and go off into the exciting world of Java and C# it's probably fine, but it's not the right book to teach someone to write production-quality C.
I wish it was a best practice that strcpy() was never used, because it would make static source code analysis a lot easier: see strcpy()? Flag it! But no: lots of excellent C code properly relies on the assumption that string literals don't change their size at runtime.
Similarly, yours is a stylistic standard for braces that rejects OpenBSD KNF. Good luck with that. You're entitled to an opinion and, on your own dev teams, it's perfectly reasonable to demand consistency with an "always use braces" style. But it's not reasonable to call style fouls on other people's code that adheres to style(9).
Really strong disagree that K&R isn't a good first book for writing production C. I could go on & on, but since I'm echoing the commanding majority of all C programmers in sharing that sentiment, there's probably no need.
I see the complaint is more about C style strings in general because it's easy to corrupt them by overwriting NULL at the end rather than a complaint about "K&R C" book. I don't get why he decided to put it under this title.
And yes, the solution is to require a size with every string buffer, which is what I do nearly all the time now. But, the best way to learn why that's solution is to try to hack something with this kind of defect.
From what I can tell, the "it's time for C to go" comes from a generation raised on Java.
C still has its place.
You see, programmers tend to go around only using functions exactly as they were intended. Hackers come along and then use them in unintended ways, which causes bugs. The way to prevent this is to think like a hacker and try to use your code in unintended ways and then try to prevent them.