Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

In my quest to learn C very well over the past few years, I've come to the conclusion that C is best understood if you think about it in terms of the way that an assembly language programmer would think about doing things. An example of this would be if you consider how switch statements work in C. Switch statements in C don't really compare to switch statements that you find in other languages (eg. https://en.wikipedia.org/wiki/Duff%27s_device).

The issue that many students face in learning low level C, is that they don't learn assembly language programming first anymore, and they come from higher level languages and move down. Instead of visualizing a Von Neumann machine, they know only of syntax, and for them the problem of programming comes down to finding the right magic piece of code online to copy and paste. The idea of stack frames, heaps, registers, pointers are completely foreign to them, even though they are fundamentally simple concepts.



> I've come to the conclusion that C is best understood if you think about it in terms of the way that an assembly language programmer would think about doing things.

I don't agree. That leads people to incorrect conclusions like "int addition wraps around on overflow" (mentioned in the article), "null pointer dereferences are guaranteed to crash the program", "casting from a float pointer to an int pointer and dereferencing it is OK to do low-level tricks", and so forth. C is a language that implements its own semantics, not the semantics of some particular machine. Confusing these two ideas has led to lots and lots of bugs, many detailed in John's other blog posts.

It might be useful to teach these intuitions to beginning programmers who already know assembly language before learning C (though are there more than a vanishingly few number of those anymore?) But teaching assembly language as part of, or as some sort of prerequisite for, teaching C strikes me as a waste of time and likely to lead to wrong assumptions that students will eventually have to unlearn.


I agree with both of you to some extent. Despite the fact that C implements its own semantics, those semantics are downright bizarre and hard to gain intuition about unless you have some mental model of the machine.

For example, here are some of the things that confused me when I first learned C:

    - why isn't there an actual string data type? (just char*)
    - why do some people use "char" to store numbers?
    - whats the deal with pointers?
    - why are pointers and arrays kinda sorta interchangeable?
Until I learned how things work at the assembly language level, I could not gain an intuitive understanding for why C works the way it does.


Understanding that requires understanding that the program's memory is an array of bytes where everything is stored. You do not need to know assembly to know that.


You don't really need to know assembly to understand those, though. Usually classes will go over the basics - how things are stored in memory, the stack and the heap, etc., and that's enough to answer those questions.


If all you tell me about the machine is its memory layout, you haven't told me nearly enough to explain the oddities of C.

For example, I could imagine a machine with identical memory layout to C, but that supported a hugely parallel, variable-size data bus where an operation like "x = y" (string assignment by value) could copy an entire string in a single operation.

The reason C doesn't support this is because generally at the assembly language layer you can only operate on memory in register-sized chunks, and every load or store of a register's worth of memory takes time. So assignment of a string by value requires a loop, just like it does in C.


I'm not sure I understand your example. Knowing how arrays (or pointers) are handled in C is enough to understand why you can't have string assignment by value. How C treats things is all you really need to know if you want to use the language. This isn't just true for C. In my experience, people tend to have more difficulty with string assignment in Java than C, but you usually don't hear people say that they need to go to a lower level to really understand Java.

Understanding the why is interesting - like it is for any language. And like in many languages, the why tends to be complicated and somewhat arbitrary at times. If you're really interested in the why rather than the what, a book on the history of C will probably be more useful than learning assembly.


> [ Knowing how arrays (or pointers) are handled in C is enough to understand why you can't have string assignment by value.]

True, but I think knowing how arrays (and pointers to arrays) work in C is one of the main hurdles for a lot of people who are just starting out.

This became especially apparent to me after recently helping my friend get familiar with C.

I can see how some people can get confused by this when you consider the fact that structures can be copied using a straight assignment, while arrays can't.

People naturally try to find similarities when learning something new, so it took a little while for him to _really_ get it. I think his mind kept trying to think of a structure essentially as an array of variables, when that's not really the case.


It is not easy, but so far the best way to wrap newcomer around most of C oddities (in this context) is to explain two things: 1) memory location and size 2) run time vs compile time.

Then it becomes apparent why one cannot copy strings "by assignment" and can structs: it is in general impossible to know runtime size of string at compile-time. C strings have no structure known at compile time. Structures, on the other hand, are there to enforce structure on data.

There is a pretty neat real-world analogy here: copy machine. String copy must be done character-by-character in a same way that book or document folder would have to be copied page-by-page. On the other hand engineering drawing must be copied whole. It may contain references to other drawings, you can still with some struggle extract individual parts, but it is copied as a whole. This analogy relies on a fact that drawings are single-page and but nicely encapsulates the "strings are arrays are pointers" idea: folder may be empty, may be single page, but it is impossible to know without attempting.


    struct { char name[30]; } tgt, src = { "Einstein" };
    tgt = src; 
Since there are no strings, it cannot be true that strings are pointers. They can be arrays, and array sizes are known at compile time. It's just a 40-year old cop-out that we can't copy by assignment all types based on their `sizeof` size.


> <...> it is in general impossible to know runtime size of string at compile-time <...>

You are stepping on the same rake as beginners: generalizing from a specific case instead of applying the general case to specific circumstances. I have stressed the word "in general". You may treat it like a cop-out or you can say that there are no special-case semantics here. Not sure if it was intentional, but your example is rather tricky. Until you step out of the box and see that we are no longer dealing with strings/arrays/pointers here, but structs, that have a bit different semantics.

> Since there are no strings

Yes, there is no explicit string type in a language, but somehow we do use strings in C. Semantics. We can semantically treat a particular block of memory as a string, time-series, binary tree, etc.. There is simply no special case (explicit language support) for strings.

> it cannot be true that strings are pointers. They can be arrays, and array sizes are known at compile time.

What about `malloc`? What about passing arrays between compilation units? I have covered this in SO[1]. Note that I never explicitly pass pointers, yet `sizeof()` thinks I do. Array sizes can, in some circumstances, be known at compile time in a specific program block, but not in general.

I'd say there are +/- 3 types of languages (core, no stdlib, etc) in this context: 1) provide common-special-case exceptions 2) wrap all cases in an easy-to-use interface 3) provide general case syntax. 1) languages with `=` and `eq` (Perl?) 2) languages with object-identity (Python?) 3) C

[1]: http://stackoverflow.com/questions/19589193/2d-array-and-fun...


> For example, I could imagine a machine with identical memory layout to C, but that supported a hugely parallel, variable-size data bus where an operation like "x = y" (string assignment by value) could copy an entire string in a single operation.

You mean like x86 (at least as far as the ISA is concerned) [1]? :)

I mean, this isn't just me being pedantic and annoying: I think it goes to show that C's machine is quite different from a real machine.

[1]: http://x86.renejeschke.de/html/file_module_x86_id_279.html


I thought about bringing this up as well. I'm not sure what you mean by "C's machine is quite different from a real machine". Do you mean (as I was thinking) that even assembly language is frequently not close enough to the machine to be used to guide the programmer trying to write high efficiency code?

I'm constantly surprised by how poorly documented the actual operation of current processors is, and how few people seem to care. In one way, this means that the abstraction is working, and no longer does anyone need to look behind the curtain. In another way, like the move to teaching only higher level languages, it feels like something essential is being lost.


What I mean is that C is defined in terms of a virtual machine with absolutely no restrictions on what happens if you step outside the boundaries of that machine's defined operations. That's in contrast to real machines, which typically have much less undefined behavior.


I actually thought of that when I wrote my comment. But still, notice that (1) the instructions take as their input registers that point to the data (ie. a char pointer), not some machine-level idea of a "string", and (2) the cost of these instructions is still O(n), even though you don't have to write the loop manually.


In a C compiler implemented for that architecture, copying a value of memory line size could very well use an instruction that does it all at once if the registers are large enough or a DMA intrinsic is exposed. Really that's the point. C is like a near-asm language that standardizes across ISAs with the opportunity for the compiler, libraries, or programmer to do something more clever on a specific system. It requires dedication and patience, but in the end is generally a good middle ground for low level work.


Copying a string by doing x = y is rare among languages. Most copy references like C does. An example of an outlier that does a string copy like you want would be C++ on its string class.

You can copy/initialize strings in C without ever writing a single loop by using strcpy. As for hardware, you need not have something so exotic. x86 for instance has a string copy instruction. The C strcpy function is often compiled to it.


Is strcpy not implemented with a loop?


To add to the oddities:

You can copy entire structures by value by doing "x = y".

This ends up being implemented as a memory copy loop, but I'm not sure what happens with the padding bytes. There's a chance they get copied as well, but I really don't know.

In the code base I work with, it's common to see arrays inside of a structure which they're the only member of. This makes it a little easier to copy them, though I'm not sure if that was the intended purpose.

Something like this would be defined in a header...

typedef struct { char myArray[50]; } Test_Struct_T;


It's not specified what happens to the memory padding bytes on structure assignment.

Hence: you can't use memcmp() to reliably compare structures for equality.


> You can copy entire structures by value by doing "x = y".

That wasn't in the original language, though it's a pretty old extension.


> That wasn't in the original language, though it's a pretty old extension.

That's why I don't recognize that feature. I guess we didn't have it back when I was doing a lot of C.


I believe C++14 adds the first sane C-family array type:

template<typename T, size_t N> class array { T data[N]; };


std::array is in C++11


> - why are pointers and arrays kinda sorta interchangeable?

Because apparently writing ptr = &arr[0] like in older systems programing languages was too hard implement.


Why do some people store chars to store numbers?


Because unlike other programming languages C didn't define a type called byte, so developers were forced to use char for what is byte in saner systems programming languages.

With the caveat that unless you precede them with signed/unsigned modifiers, it is not portable.


Because uint8_t is comparatively new. C99, IIRC, though many OSes defined their own earlier.


They need a char-sized number, and memory used to be a tightly-budgeted commodity.

Reminds me of this story of a game developer who magically got their game under the limit at the 11th hour

http://www.dodgycoder.net/2012/02/coding-tricks-of-game-deve...


Although char should not be used to store numbers, since its signedness is implementation-defined (gcc has the switch -funsigned-chars I think to make them unsigned instead of signed). signed char/unsigned char are ok.


You're right that trusting the assembly intuitions leads to danger when confronted with a standard compliant optimizing compiler. But on the other hand, unless one understands how processors actually execute code, I don't think it's possible to write high performance C.

And if one isn't writing for high performance, probably one shouldn't be using C. I like the suggestion in a sibling about teaching C in the context of programming a microcontroller. I think this might bridge the gap a bit: encourage the right intuitions, without creating dangerous misconceptions.


Very true. I counter this cross-platform assembly notion whenever I can as they're not the same thing. One reason for its weirdness, also only assembly it's tied to, is that it was specifically designed to utilize a PDP-11 well. It and anything depending on it is mapped to what makes sense on a PDP-11. We don't use PDP-11's today.

So, the C model neither makes sense nor matches assembler of today's CPU's. We've certainly developed ways to implement it efficiently. It wasn't designed for that, though.


Why is the PDP-11 so different from today's CPUs, and what about it made C implementations for it more efficient? The only thing I can think of is postincrementing registers. Other than that, the instruction set seems to me to be remarkably close to what you could see on a contemporary CPU.


A contemporary x86, RISC, mixed (ISA + accelerators), or what other CPU? I think CPU is a broad term. :) Anyway, Wikipedia has a detailed write-up that assembly experts can base a comparison on:

https://en.wikipedia.org/wiki/PDP-11_architecture

It wasn't that PDP-11 made C implementations more efficient. It's that C was a BCPL specifically designed to compile easily and run fast on their PDP-11. That's why I can't overemphasize C's actual history vs the lore that people repeat. It's literally an ALGOL language with every feature that couldn't compile on 60's and 70's era hardware chopped off with some extensions added latter.

http://pastebin.com/UAQaWuWG

Worked fine for a PDP-11. Yet, forcing its memory model or tradeoffs into a language used on different hardware can cause unnecessary problems. In contrast, Hansen's Edison language deployed on PDP-11 had only five statements (extreme simplicity haha) but would map efficiently to most architectures. As would Pascal and Modula-2 that inspired it & were safer.

http://brinch-hansen.net/papers/1981b.pdf

https://en.wikipedia.org/wiki/Modula-2


> A contemporary x86, RISC, mixed (ISA + accelerators), or what other CPU? I think CPU is a broad term. :) Anyway, Wikipedia has a detailed write-up that assembly experts can base a comparison on:

Contemporary x86 and RISC CPUs are what I was comparing the PDP-11 instruction set to. I don't see any fundamental differences. Painting with very broad strokes, the PDP11 ISA looks reasonably close to x86. And those minor differences in more modern RISC actually map better to C than the PDP 11 does -- for example, status flags being replaced with jumping based on register contents. Implicit widening to words is a weak mismatch for x86, but it's a pretty good match for modern risc (no need to mask out top bits in registers), etc.

I looked at your links, and I'm still not seeing how other C maps better to a PDP-11 than it does to modern CPUs. The only thing I'm seeing in the pastebin rant is that CPUs are fast enough and memories are big enough today to support more expensive features, which I can agree with.

Again:

> Worked fine for a PDP-11. Yet, forcing its memory model or tradeoffs into a language used on different hardware can cause unnecessary problems.

What parts of its memory model or tradeoffs made it into C? I can't find any specifics that you're basing these claims on, only assertions that it's true.

In fact, the usual complaint associated with C is that it left the memory model so loosely specified -- initially to allow it to match any hardware -- that optimizing compilers can use the looseness to do really strange things to your code.


"The only thing I'm seeing in the pastebin rant is that CPUs are fast enough and memories are big enough today to support more expensive features, which I can agree with."

Fair enough haha. Ok, my memory loss is hurting me on examples. I might have just been the little things adding up. I do recall two from security work: reverse-stack and prefix strings. MULTICS, UNIX's predecessor, had both with significant reliability and security benefits. Reason C had null-terminated strings was PDP-11's hardware and one personal preference/opinion:

"C designer Dennis Ritchie chose to follow the convention of NUL-termination, already established in BCPL, to avoid the limitation on the length of a string caused by holding the count in an 8- or 9-bit slot, and partly because maintaining the count seemed, in his experience, less convenient than using a terminator."

Now, on reverse stack, my memory is cloudy. Common stacks have incoming data flow toward the stack pointer in a way that can clobber it, even leading to hacks. MULTICS had data flow away from the stack pointer with an overflow dropping into newly allocated memory or raising an error. C language (and most) implementations use regular stack. I think it was because PDP hardware expected that with a reverse stack requiring high-penalty indirection. I could be wrong, though. I know a reverse stack on x86 gets a performance penalty and key traits of x86 come from PDP-11. A CISC with reverse stack would have problems with C.

The pointer stuff. Lots of the pointer stuff, esp arrays, comes from efficiency needs for running on a PDP-11. This by itself is why we can't map C easily to safer or high-level hardware. The CPU at crash-safe.org, jop-design.com, and Ten15 VM come to mind. PDP-11 model doesn't support safety/security so neither does C.

These are a few that come to mind that carry over into modern work trying to go against C's momentum. Hardware, software, and compiler work.


> Now, on reverse stack, my memory is cloudy. Common stacks have incoming data flow toward the stack pointer in a way that can clobber it, even leading to hacks.

That's not a restriction of C, but a way to get more out of your memory on a restricted system; If your heap grows up and your stack grows down (or vice versa), then you can keep using growing both until the two meet, at which point you've used all the available memory. However, if they both grow in the same direction, you need to statically decide how much to give each one, which will lead to waste if you're not using much stack or heap:

    [heap-->|           |<--stack]
vs:

    [heap-->|    |stack-->|      ]
But, again, not something that C cares about; you have a number of architectures like Alpha (IIRC) where the program break and the top of stack move in the same direction.


Gotcha. Appreciate the tip.


Can you explain what you mean by a reverse stack? Is that a stack that grows upwards like the heap? Why does this incur a penalty?


I originally learned about it and other issues in a paper by the people (Schell & Karger) that invented INFOSEC:

https://www.acsac.org/2002/papers/classic-multics.pdf

Really old stuff. Relevant quote: "Third, stacks on the Multics processors grew in the positive direction... if you actually accomplished a buffer overflow, you would be overwriting unused stack frames rather than your own return pointer, making exploitation much more difficult."

I can't find the original paper showing the penalty on x86/Linux. However, this one does the same thing for different reasons with many details:

http://babaks.com/files/TechReport07-07.pdf

Key point: "The direction of stack growth is not flexible in hardware and almost all processors only support one direction. For example, in Intel x86 processors, the stack always grows downward and all the stack manipulation instructions such as push and pop are designed for this natural growth."

So, on such an architecture, you can't directly use the stack operations to do the job: must implement extra instructions without hardware acceleration. The stack on x86 is effed-up and insecure by design. If C's stack is fixed, there's still a mismatch between it and x86 ASM. Itanium at least provided stack protection among other security benefits.


Interesting. Thanks for the links. Isn't C's stack pretty much tied to the hardware? Curious what you mean by "If C's stack is fixed"? How could that be implemented, changing the run time?


You change the compiler to emit different things like a reverse stack or whatever your protection model is. Far as implementation, they describe it in p5 (PDF p7) of paper above (not MULTICS paper). It's actually brilliant now that I read it as the naive thing they avoid is, IIRC, what the other academics did on Linux/GCC. The performance overhead hit 10% easily due to x86's stack approach. I think worst-case was even higher. This team effectively tricks the CPU with simple instructions (eg addition/subtraction) without invoking memory to get it to worst-case of 2%. Clever.

Note: I'm not saying this is sufficient to stop stack smashing. Just that reverse stacks are a better idea than the ludicrous concept of making unknown amount and quality of data flow toward the stack pointer. Definitely reduced risk a bit but how much takes more assessment.


Interesting stuff, thanks.


I'm gonna go out on a limb and guess "memory model"; both consistency and coherence issues.


In that case, I'm not really aware of any language that exposes this to the user, outside of assembly.


Many languages restrict or eliminate the use of pointers. C doesn't. So, it exposes whatever model it expects underneath to its users.


I think you do have a point about possibly misleading people in relation to signed overflow, since that is common hardware behaviour, but for the point about null pointer de-reference, I believe thinking in terms of assembly would actually help people realize that referencing memory location 0 (if that is what null was defined to), will not immediately crash your program. It is not special, it's just another memory location.

Your point about teaching assembly as being a waste of time also has some merit. Of course people still do program in assembly, but it is only becoming more and more rare to actually need someone to program assembly, which is why it just isn't emphasized as much any more and it makes C seem like an even stranger language for someone who started on Python or Javascript.


Because dereferencing a null pointer is undefined behavior, the compiler is free to assume it won't ever happen. This, in turn, can lead to the compiler optimizing or reordering code in a counter-intuitive manner, completely changing the behavior of a program that you thought you understood.

The compiler might emit machine code that attempts to read the memory at location 0. It would also be perfectly within its rights to optimize away that branch of code (if it can prove that it always dereferences null). The code might even appear to work now, and completely break in the next version of the compiler.


The page containing address zero (NULL) is almost always unmapped in environments with virtual memory, specifically to catch NULL pointer dereferences. That's not the source of the problem. The problem is that dereferencing a NULL pointer is "undefined behavior," and modern compiler writers abuse every instance of undefined behavior to implement negligible optimizations that subtly break programs for little gain.


> I believe thinking in terms of assembly would actually help people realize that referencing memory location 0 (if that is what null was defined to), will not immediately crash your program. It is not special, it's just another memory location.

No, it is special. Dereferencing it is undefined behavior. It is not guaranteed to result in a load at address zero.


The cited incorrect assumption was "null pointer dereferences are guaranteed to crash the program", which is, as you implied, not true because it won't necessarily crash the program. C places additional restrictions which say that the program can actually do anything (undefined behaviour).

The main reason I keep coming back to assembly language with C, is that C cannot do anything that assembly language cannot do. It only places additional restrictions on assembly, which is a bit easier to grasp (bitwise operations, add, subtract etc.). Once you understand the fundamental operations of the processor, you can start to learn the copious corner cases that is the C programming language.


> The main reason I keep coming back to assembly language with C, is that C cannot do anything that assembly language cannot do.

That's basically true. Except that an optimizer is allowed to transform things as long as the observable behavior remains the same. And undefined behavior means more than "just that one line is undefined" ( http://blog.llvm.org/2011/05/what-every-c-programmer-should-... , http://blog.llvm.org/2011/05/what-every-c-programmer-should-... , http://blog.llvm.org/2011/05/what-every-c-programmer-should-... ). These two facts conspire to make undefined behavior surprising to many programmers.

For instance -- using one of Lattner's examples -- on some architectures it's a little expensive to check if a loop variable had wrapped around. So the optimizer will omit the check for wraparound if it can prove wraparound is impossible. That is, if it can prove that n + 1 > n. But the Standard only requires wraparound for unsigned integer types. So the optimizer can also omit the check if it can only prove either n + 1 > n or n is a signed integer. In this case, a bounded loop turns into an infinite loop, but "undefined behavior" includes that kind of transformation.

More to the point, Linux had a severe security bug where they dereferenced a pointer, then checked if the pointer was NULL before returning the value ( https://lwn.net/Articles/342330/ ). The optimizer removed the check for NULL because if the pointer was valid when it was dereferenced, the check was unnecessary; and if the pointer was NULL when it was dereferenced, then dereferencing it was undefined behavior and "remove an NULL check" is a valid transformation in undefined behavior. Then moving that load to a different point in the function is also a valid transformation (as long as it doesn't affect observable behavior), and a few more transformations could make the NULL dereference do something completely different from what you expect.


> C cannot do anything that assembly language cannot do.

I know where you're going with this, but I don't agree with this statement. C provides the abstraction of structs and unions, which do not exist in assembly. Arrays are also an abstraction which does not exist in assembly - yes, assembly provides the mechanisms to easily compute addresses for arrays, but that is different than having a named entity for many objects. The direction you're going is that C is a thin abstraction over assembly, which I agree with. But that is different than saying C has a one-to-one mapping, which is what I think your statement implies.


With modern compilers and modern CPUs, C isn't a particularly thin abstraction. Many serious misconceptions stem from naive assumptions about how C will be translated into assembly which are invalidated through compiler code transformations- often with results that surprise even expert compiler maintainers. Please do not spread the idea that C has any straightforward or predictable relationship with assembly language.


"more straightforward and more predictable relationship with assembly language than any other language in the world" is the right way to say it.


That's a rather bold assertion. How would you back it up? What about high-level assembly languages, ISA-agnostic IRs or languages like Forth or Oberon for which compilers intentionally perform only simple optimizations, if any?


My name is Robert Elder and I approve this message.


Has anyone ever seen any behaviour other than a segfault in the wild, though? Would any real-world compiler author decide that some other behaviour was reasonable? I can't imagine making that decision myself.


A classic problem is code like this:

    printf("a=%p *a=%d", a, *a);
    if (a == NULL) return;
An optimizing compiler will likely remove the NULL check completely because the printf above it has undefined behavior if a is NULL.


On some microcontrollers I know a pointer to address 0 really is just that. I tested compiling the following

    unsigned short *a = 0;
    ((void (*)(void))a)();
which may cause a jump to address 0 (usually the reset vector).

On R8C with the NC30 compiler there are no warnings and the output is:

    MOV.W:Q #0H, -2H[FB]
    MOV.W:G -2H[FB], R0
    MOV.W:Q #0H, R2
    JSRI.A  R2R0
It jumps to 0. But on PIC 16 with the XC compiler a warning is emitted warning: (1471) indirect function call via a NULL pointer ignored and the statement is not compiled at all.


On BSD on the VAX, address 0 used to happen to hold 0, so dereferencing NULL got you zero. That was at the time a sufficiently popular platform that some programs assumed the behaviour (eg, that a null pointer could be used as an empty string), and you can still find older docs on the web exhorting the reader to avoid "all the world's a VAX" syndrome.


I think on DOS in real mode address 0 was not special. Some(?) compilers would add code to programs to check that location on program exit to see if its value had changed and emit an error.

(I think I read about this somewhere, but it was a while ago, and I never really programmed for DOS (well, one little program to set the serial port to some specific settings, but that was about ten lines including whitespace and a little comment explaining what the code did).)


On MacOS Classic (no memory protection) you can happily write to address zero and replace whatever is stored there (I think Apple wisely left that memory address unused, though).


Of course it has its own semantics. I've seen the idea of "the 'C' virtual machine". You got a running start on the differences it has from most assemblers ( each of which will have semantic differences from each other ).

I think his point that "The idea of stack frames, heaps, registers, pointers are completely foreign to them..." is the critical point.


It still seems easier to present C as "a bit like assembly except for..." than "a bit like Java except for...".


>I've come to the conclusion that C is best understood if you think about it in terms of the way that an assembly language programmer would think about doing things

I agree with this. I found another article that made the transition for someone coming from something like Python or Ruby easier. It shows how to use gdb as a sort of C REPL. https://www.recurse.com/blog/5-learning-c-with-gdb

The quick feedback loop certainly makes things like pointers, arrays, etc, more clear.


Wow. I've been flirting around with learning C for awhile now (coming from a couple dynamic languages, including python), and this is by far the most helpful read about pointers I've ever come across. Thanks a ton.




Heck yeah. Thanks for the tip. Btw, new link is here:

https://github.com/zsaleeba/picoc


+1 to this. GDB and valgrind are the best things about writing C.


Valgrind is the best thing since sliced bread - saved me so much time and agony...


One things that I've found enlightening is when you have an IDE that allows you to step through mixed source and assembly. You can see your data being slopped around and processed. (int a = 10; becomes ld R4,10)


Personal anecdote: C was the first language I learned while I was in middle school. I taught myself. I admit that I have gained a deeper understanding of C after I recently took Computer Architecture class, but I don't think learning assembly is essential to understanding the C language, stack/heap, and pointers. When I was first learning C, a simple memory diagram with simple description was sufficient. Perhaps the problem is learning a high level language first (most schools start with python or java)? Maybe students struggle with the transition or it isn't explained well enough. I couldn't say since I started with C. I'm interested if anyone had this problem because I tutor students.


My experience was similar. I taught myself C during high school, and found transitioning to higher level languages to be fairly simple after that. C was taught as one of the later languages in my university course, and many of my friends (that started with higher level langauges) struggled to pick it up.


> The issue that many students face in learning low level C, is that they don't learn assembly language programming first anymore, and they come from higher level languages and move down. Instead of visualizing a Von Neumann machine, they know only of syntax, and for them the problem of programming comes down to finding the right magic piece of code online to copy and paste. The idea of stack frames, heaps, registers, pointers are completely foreign to them, even though they are fundamentally simple concepts.

What about teaching C in the context of something like AVR programming, where you have to worry about those sort of things because there simply isn't any abstraction on top? That was where I first encountered C and I think learning it in such a constrained environment helped me appreciate/understand the utility of C a lot more.


Pascal and Basic are also an option when targeting AVR:

http://www.mikroe.com/mikropascal/avr/

http://www.mikroe.com/mikrobasic/avr/


To add to the others noting the differences between C's behaviour and the underlying assembly, I recommend Chris Lattner's (the creator of LLVM) series of posts about Undefined Behaviour in C:

http://blog.llvm.org/2011/05/what-every-c-programmer-should-...

http://blog.llvm.org/2011/05/what-every-c-programmer-should-...

http://blog.llvm.org/2011/05/what-every-c-programmer-should-...

(HN truncates the text of the URL, but they're all different)

I've been coding C professionally for over a decade, as required for firmware/embedded development, and those posts have instilled the fear of god in me.


> ... and those posts have instilled the fear of god in me.

But why? Yeah, hitting UB can be a terrifying idea but rarely happens in practice.

In two decades of C programming, I have hit an UB bug exactly once when a piece of code was ran on an ARM platform for the first time. It took a little bit of staring at disassembly and reading some docs to sort out but it wasn't the end of the world.

Understanding the basic cases is a good idea but the darkest corners of undefined behavior are only important if you're a compiler writer like Chris Lattner is.


A solid understanding of undefined behavior is required if you want to write software in C that is secure, that will behave properly, and that is portable.

Undefined behavior has been the source of numerous security exploits in the past, and will only get worse as modern optimizing compilers become more advanced.


I took to C like a duck to water, probably because I was just coming from having extensive experience programming the PDP-11 in assembler, and C looks and behaves a lot like 11 assembler. (Such as integral and floating point promotion rules.)


> In my quest to learn C very well over the past few years, I've come to the conclusion that C is best understood if you think about it in terms of the way that an assembly language programmer would think about doing things. An explosion ample of this would be if you consider how switch statements work in C.

Require students to write a toy operating system and compiler in C. They will understand things obscenely well by the time that they are finished.

Another option is to introduce students to DTrace and require them to use it to answer questions about kernel and userspace behavior. Ask the right questions and they will learn all of the things you want them to learn from the process of answering the questions.


>and for them the problem of programming comes down to finding the right magic piece of code online to copy and paste.

This seems like a particularly uncharitable thing to say about people who write code in a 'high level' programming language. You're alleging not just that they don't understand the fundamental low level workings of a computer, but that they don't even understand how to write new programs in their language?


Uncharitable...probably.

Though there is some truth to that. It's far more likely that copy-and-paste X is a workable solution in a higher-level language than a lower-level one.

A high-level language emphasizes portability and abstraction; a low-level languages emphasizes performance and implementation details.

So...the comment sounds harsh but in reality is a reflection of the success of high-languages.


When you want to fully grok C, knowing some assembly and techniques does help. It's irrelevant for those learning the language. They don't need to know about CPU saving effects from fallthrough or lazy evaluation. They're still busy tripping up mixing arrays and pointers and incrementing the wrong one, null pointer assignments or forgetting the break in the switch. When you have some years time and are exploring the edges of the language that knowledge undoubtedly helps.

It was rare for students 25 years ago to learn assembly first. Can't say it made it harder to learn or that students had issue (spent a couple of years in the 90s teaching C part time to contractors). They had issue with language beginner things. Pointer arithmetic, or confusing pointers/arrays, but can't ever remember anyone having a particular issue with switch. Fall through was a C thing, they accepted it quite happily, and forgot break sometimes as learners do. People seemed to have far, far more difficulty getting comfortable with C++ and OO than C. The new C++ programmer was much more dangerous than the new C programmer!

C was often taught as first "real" language. You'd introduce pointers and here's how that aspect of computers work as part of the same scribble on the whiteboard. Same for memory allocation, stacks, heaps and byte sizes/packing. The fact that C was so directly close to those concepts made grasping them that much easier.

We lost a lot when we moved beyond expecting people to be aware of those basics. PHP isn't even sure itself what data is. Being able to pack your data or have app data that's optimal would be appreciated by those "few" smartphone users outside SV where dropping data or fallback to GPRS happens often. Data is rarely thought of in terms of size, it's just a blob of some types/objects. Little surprise when the app spits JSON of epic size and spends half its time "thinking".


> Little surprise when the app spits JSON of epic size and spends half its time "thinking"

At least it's not XML...


It's wonderful that the article mentions the Duff's device. I still remember the mind-boggling effect when I first met it. It taught me many things. Once you understand it you will never make mistakes with switch/case in C anymore.


C really only clicked for me when I was taking my computer architecture class in college - working through the Patt & Patel book[1], along with the Tanenbaum book[2], and building an entire 8-bit CPU from the gates up in a simulator.

I've seen so many people that just can't wrap their heads around pointers, but it makes so much more sense when you've gotten down to the nitty-gritty level and built up from there.

[1] http://www.amazon.com/Introduction-Computing-Systems-gates-b...

[2] http://www.amazon.com/Structured-Computer-Organization-Andre...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: