For "dark corners of C", when I was writing C code I had several serious concerns. Below I list eight such in roughly descending order on 'seriousness': First, what are malloc() and free() doing? That is, what are the details, all the details and exactly how they work? It was easy enough to read K&R, see how malloc() and free() were supposed to be used, and to use them, but even if they worked perfectly I was unsure…
I think C might make more sense if you are more familiar with assembly language. I learned C because real-mode x86 looked so fantastically ugly (looking back, a rare instance of youthful good taste). 0-terminated strings and stack allocation were quite familiar to me (though I never used stack allocation myself because it made the disassembly hard to read) and the overall model made perfect sense.
Some dark corners of C
141–150 of 175 posts
Re: Some dark corners of C
#142int x = 'FOO!'; will not make demons fly out of your nose: it is not undefined behaviour . It is guaranteed to produce a value; the specific value is implementation defined (that is, one that the compiler vendor has decided and documented), but it is an integer value, not a demon value. I'm sure, though, that someone sooner or later will be bitten by code like int x = 'é'; which is equally implementation-defined.
On big-endian machines, the order of characters is preserved. Because of that, I've noticed this trick used in old network/protocol code where the intent was to use integer values in binary headers while maintaining easy readability if you are look at hex/ascii side-by-side. e.g., int x = 'RIFF'; .. if you were packing a WAVE file header.
Re: Some dark corners of C
#143Earlier quoted context omitted.
Not to forget #define struct union which will pack all struct members together. Gives me chills even to think about it...
The #define else one is scarier to me. #define struct union will almost certainly crash immediately on runtime, as one member's a pointer and it gets overwritten by an int or a pointer to a different type. #define else just always runs all else clauses - the code is likely still valid, it just does something very different from what the author intended. Both of them would be a real pain to debug.
Re: Some dark corners of C
#144Earlier quoted context omitted.
Last time this came up on HN, it was thought to be a clang feature. Edit: While we're talking about dark corners, please stop casting functions that return void * . If your code lacks the declaration of the function, the compiler will assume pre ANSI-C semantics and generate code returning an int . On machines where pointers do not fit ints (basically all 64bit machines), you just silently (due to the cast there is n…
> If your code lacks the declaration of the function, the compiler will assume pre ANSI-C semantics and generate code returning an int. Better, please help by compiling your C code with -Wimplicit-function-declaration (included in -Wall), and fixing all the problems it reports. Then you won't have to worry about this problem, or a bunch of other problems.
Re: Some dark corners of C
#145Earlier quoted context omitted.
If you ever program in C again, use valgrind to help debug your programs: http://valgrind.org/ It's particularly useful for bugs related to memory.
Thanks. I just made a note of that! So, someone else dug into the details of how C manages memory and wrote some code to help people find problems; makes good sense.
But how C programs behave regarding memory is well known and understood. However, it does require understanding basic memory concepts related to the operating system itself.
Re: Some dark corners of C
#146Earlier quoted context omitted.
It replaces the first word with what comes after. For example, #define foo bar is roughly equivalent to s/foo/bar/g, so #define else would just remove all else keywords. I'll let you guess what it does to a program.
Not to forget #define struct union which will pack all struct members together. Gives me chills even to think about it...
#define memmove memcpy
#define strncmp memcmp
#define rand fork
#define free(x)
#define strerror(n) strerror((n)+1)
#define memset(addr, byte, len) memset(addr, ~(byte), len)Re: Some dark corners of C
#147Earlier quoted context omitted.
It won't execute though: [23] .got.plt PROGBITS 0804954c 00054c 000014 04 WA 0 0 4 [24] .data PROGBITS 08049560 000560 000010 00 WA 0 0 4 The main symbol is a relocation in .data, not .text. Which is as you would expect given that declaration. You might be able to get around that by doing something like unsigned char code[] = { 0xf0, 0x0f, 0xc7, 0xc8, 0xc3 }; int main(void) { ((void (*)())code)(); return 0; } But the…
Works if you add this line: char main[] __attribute__((section(".text"))); (You get a warning from the assembler.)
I didn't know gcc attributes included that kind of thing. I've really gotta dig though the manual some time.
Re: Some dark corners of C
#148For "dark corners of C", when I was writing C code I had several serious concerns. Below I list eight such in roughly descending order on 'seriousness': First, what are malloc() and free() doing? That is, what are the details, all the details and exactly how they work? It was easy enough to read K&R, see how malloc() and free() were supposed to be used, and to use them, but even if they worked perfectly I was unsure…
1) malloc() and free() are just library calls, they're not first class citizens of the language. K&R and other good C references describe their public interface well and that's all you need to know to use them effectively. The public interface encapsulates the implementation details, good software engineering in my book. 2) Usage of the stack reflects C's low-level, high performance "portable assembler" roots. Choosi…
On your
"K&R and other good C references describe their public interface well and that's all you need to know to use them effectively."
I want more. By analogy, all you need to drive a car is what you see sitting behind the steering wheel, but I also very much want to know what is under the hood.
Generally I concluded that for 'effective' 'ease of use', writing efficient code, diagnosing problems, etc., I want to know what is going on at least one level deeper than the level at which I am making the most usage.
Your example of putting a 100,000 byte array on the stack is an example: Without knowing some about what is going on one level deeper, that seems to be an okay thing to do.
2) My remark about the stack is either not quite correct or is not being interpreted as I intended. For putting an array on a push down stack of storage, I am fully aware of the issues. But on a 'stack', maybe also the one used for such array allocations (that PL/I called 'automatic'; I'm not sure there is any corresponding terminology in C), there is also the arguments passed to functions. It seemed that this stack size had to be requested via the linkage editor, and if too little space was requested then just the argument lists needed for calling functions could cause a 'stack overflow'. A problem was, it was not clear how much space the argument lists took up.
Then there was the issue of passing an array by value. As I recall, that meant that the array would be copied to the same stack as the arguments. Then one array of 100,000 bytes could easily swamp any other uses of the stack for passing argument lists.
But even without passing big 'aggregates' by value or allocating big aggregates as 'automatic' storage in functions, there were dark threats, difficult to analyze or circumvent, of stack overflow. To write reliable software, I want to know more, to be able to estimate what resources I am using and when I might be reaching some limit. In the case of the stack allocated by the linkage editor for argument lists, I didn't have that information.
3) Sure, I could make use of the strings in C as C intended just as you state, just for textural data, but also have to assume a single byte character set.
I thought that that design of strings was too limited for no good reason. That is, with just a slightly different design, could have strings that would work for text with a single byte character set along with a big basket of other data types. That's what was done in Fortran, PL/I, Visual Basic .NET, and string packages people wrote for C.
The situation is similar to what you said about malloc(): All C provided for strings was just a pointer to some storage; all the rest of the string functionality was just in some functions, some of which, but not all, needed the null termination. So, what I did with C strings was just use the functions provided that didn't need the null terminations or write my own little such functions.
As I mentioned, I didn't struggle with null terminated strings; instead right from the start I saw them as just absurd and refused ever to assume that there was a null except in the case when I was given such a string, say, from reading the command line.
It has appeared that null terminated strings have been one of the causes of buffer overflow malware. To me, expecting that a null would be just where C wanted it to be was asking too much for reliable computing.
3) On casts, we seem not to be communicating well.
Data conversions are important, often crucial. As I recall in C, the usual way to ask for a conversion is to ask for a 'cast'. Fine: The strong typing police are pleased, and I don't mind. And at times the 'strongly typed pointers' did save me from some errors.
But the question remained: Exactly how are the conversions done? That is, for the set D of 'element' data types -- strings, bytes, single/double precision integers, single/double precision binary floating point, maybe decimal, fixed and/or floating, and for any distinct a, b in D, say if there is a conversion from a to b and if so what are the details on how it works?
One reason to omit this from K&R would have been that the conversion details were machine dependent, e.g., depended on being on a 12, 16, 24, 32, 48, or 64 bit computer, signed magnitude, 2's complement, etc.
Still, whatever the reasons, I was pushed into writing little test cases to get details, especially on likely 'boundary cases', of how the conversions were done. Not good.
Sure, this means that I am a sucker for using a language closely tied some particular hardware. So far, fine with me: Microsoft documents their software heavily for x86, 32 or 64 bits, from Intel or AMD, and now a 3.0 GHz or so 8 core AMD processor costs less than $200. So I don't mind being tied to x86.
On PL/I: Thankfully, no, it was not nearly the first language I learned. Why thankfully? Because the versions I learned were huge languages. Before PL/I I had used Basic, Fortran, and Algol.
PL/I was a nice example of language design in the 'golden age' of language design, the 1960s. You would likely understand PL/I quickly.
So, PL/I borrowed nesting from Algol, structures from Cobol, arrays and more from Fortran, exceptional condition handling from some themes in operating system design, threading (that it called 'tasking' -- current 'threads' are 'lighter in weight' than the 'tasks' were -- e.g., with 'tasks' all storage allocation was 'task-relative' and was freed when the task ended), and enough in bit manipulation to eliminate most uses of assembler in applications programming. It had some rather nice character I/O and some nice binary I/O for, say, tape. It tried to have some data base I/O, but that was before RDBMS and SQL.
In the source code, subroutines (or functions) could be nested, and then there were some nice scope of name rules. C does that but with only one level of nesting; PL/I permitted essentially arbitrary levels of nesting which at times was darned nice.
Arrays could have several dimensions, and the upper bound and lower bound of each could be any 16 bit integers as long as the lower was A structure was, first-cut, much like a struct in C, that is, an ordered list of possibly distinct data types, except each 'component' could also be a structure so that really was writing out a tree. Then each node in that tree could be an array. So, could have arrays of structures of arrays of structures. Darned useful. Easy to write out, read, understand, and use. And dirt simple to implement just with a slight tweak to ordinary array addressing. So, it was just an 'aggregate', still all in essentially contiguous, sequential storage. So, there was no attempt to have parts of the structure scattered around in storage. E.g., doing a binary de/serialize was easy. The only tricky part was the same as in C: What to do about how to document the alignment of some element data types on certain address range boundaries.
Each aggregate has a 'dope vector' as I described. So, what was in an argument list was a pointer to the dope vector, and it was like a C struct with details on array upper and lower bounds, a pointer to the actual storage, etc.
PL/I had some popularity -- Multics was written in it.
For C, PL/I was solid before C was designed. So, C borrowed too little from what was well known when C was designed. Why? The usual reason given was that C was designed to permit a single pass compiler on a DEC mini-computer with just 8 KB of main memory and no virtual memory. IBM's PL/I needed a 64 KB 360/30. But there were later versions of PL/I that were nice subsets.
It appears that C caught on because DEC's mini computers were comparatively cheap and really popular in technical departments in universities; Unix was essentially free; and C came with Unix. So a lot of students learned C in college. Then as PCs got going, the main compiled programming language used was just C.
Big advantages of C were (1) it had pointers crucial for system programming, (2) needed only a relatively simple compiler, (3) had an open source compiler from Bell Labs, and (4) was so simple that the compiled code could be used in embedded applications, that is, needed next to nothing from an operating system.
The C pointer syntax alone is fine. The difficulty is the syntax of how pointers are used or implied elsewhere in the language. Some aspects of the syntax are so, to borrow from K&R, 'idiosyncratic' that some examples are puzzle problems where I have to get out K&R and review.
To me, such puzzle problems are not good.
I will give just one example of C syntax:
i = j+++++k;
Right: Add 1 to k; add that k to j and assign the result to i; then add one to j. Semi-, pseudo-, quasi-great.
I won't write code like that, and in my startup I don't want us using a language that permits code like that.
Re: Some dark corners of C
#149Earlier quoted context omitted.
You got me! I wrote a little Algol 60 at one time and heard nice things about Algol 68 but never looked at it. Since heap is the word used in heap sort, it's fair to say that the second use of that word was a misuse. I don't know which use was second and don't really care but did want to know the details of the dynamic memory allocation used by the C malloc() and free(). I just would have appreciated an explanation o…
Um ... but K&R did. Chapter 8, section 7, "A Storage Allocator". Yes, it's simple, but it's there.
Maybe that's what was being used in the common versions of C. If so, then for whatever reason I missed out on that. I kept seeing in the book where they kept saying that malloc() allocated storage in the 'heap' without being clear on what they meant by a 'heap' although in this thread is an explanation that 'heap' was also used in Algol 68. Whatever, when they said 'heap' with no explanation, they blew me away.
Once I was one of a team of three researchers that did a new language. Eventually it shipped commercially. We needed a lot in dynamic storage allocation. Our approach was to start with an array of pointers, say, for i = 1, 2, ..., 32 or some such, s(i) was a pointer to the start of storage for chunks of size 2^(i-1) + 1 to 2^i. So, allocate 10 bytes of storage from 16 bytes where 16 = 2^i for i = 4. That is, i = 4 handles requests of size 9 through 16. Etc.
So, right away at the start of execution for relatively large j, have the operating system allocate a block of storage of size 2^j. Then for i For each i, the allocated blocks are chained together in a linked list and so are the free blocks. So, for an allocation, look first at the end of the linked list of free blocks.
In principle, after enough uses of, call it, free(), could return some storage for i to the storage for i + 1 but we didn't bother doing that.
It always seemed to me that on a virtual memory system where the page size was a power of 2 (aren't they all?), this approach to dynamic memory allocation would be quite good.
Later I was using an old version of Fortran, got a big block of storage from the operating system as just an array (right, as a common block known to the linkage editor), and wrote code such as above to have versions of malloc() and free().
If what is there in K&R in 8.7 is what was actually being used in the versions of C I used, then I blew it by not writing some code at least to report on storage allocated, freed, 'fragmentation', when allocated, etc. Basically I was highly concerned that in a relatively complicated program with just malloc() and free() I would make some mistakes in storage allocation and get some bugs that gave symptoms only occasionally and that would be a total pain to diagnose. "Last Tuesday after running for four hours we got some strange data in the file and then it blew up." Great! It reminds me of one of those arrangements of a few thousand dominoes on edge where when one tips over they all go, a house of cards, an electric power system with no circuit breakers, a dense city built with no fire safety codes, etc.
Re: Some dark corners of C
#150#define struct union #define else That's evil. I have to do it in someone's code some day just to have some fun. But, apart from that, it's a really nice compilation. I didn't know about the compile time checks of array sizes, but I have a doubt. What if I pass to a method declared int foo(int x[static 10]) this pointer int* x = (int*) calloc(20, sizeof(int)); Does the compiler skip the check? Does it give me a warni…
Last time this came up on HN, it was thought to be a clang feature. Edit: While we're talking about dark corners, please stop casting functions that return void * . If your code lacks the declaration of the function, the compiler will assume pre ANSI-C semantics and generate code returning an int . On machines where pointers do not fit ints (basically all 64bit machines), you just silently (due to the cast there is n…