Understanding C by learning assembly
hackerschool.com
hackerschool.com
* Knowing what the machine is doing is useful and important, especially while debugging. I think every really good C coder I know has been comfortable with assembly at least on some platform.
But…
* C is not a fancy macro assembler. C's behavior drives an abstract machine, and any machine code that achieves the same output assuming that all the C behavior is defined is equally valid. The believe that compiler is just transliterating your code to ASM is responsible for many serious bugs. C Compilers haven't been like that for 20 years (or more). E.g. aliasing rule violations. The fact that it isn't is utterly essential for performance, especially as things like good SIMD use become more critical for performance.
Even when the compiler is mostly transliterating the expectations from the simplified mental machine model can be misleading, e.g. alignment requirements on load on some architectures.
$ CFLAGS="-g -O0" make simple
cc -g -O0 simple.c -o simple
$
This is so handy. I never knew that you could call make without writing a default Makefile. Thanks!redo makes everything much simpler, more consistent, more dependable, and more robust. e.g all files are atomically replaced; dependencies are checked by a crypto hash of the content; dependency setup is sane; and it's all faster than make.
Thanks for the info, I'll look into redo. I just watched a recent talk about Shake (make-like in Haskell) interesting results like 10x smaller ~makefiles and 2x speed improvement.
A redo specification is generally much shorter[1] than the equivalent Makefile, yet simpler to write, can be guaranteed (unlike make / make depend) to rebuild whenever necessary and only when necessary. And while a supersmart dependency tracking incremental build version is not trivial, it's probably an order of magnitude or two shorter than make; And a 150 line bash script is enough to interpret the same specification without regard to dependencies or prior builds (that is: rebuild everything on every attempt).
echo 'void main() { printf("omg\n"); }' > simple.c
echo 'CFLAGS=-g' > Makefile
make simple
./simple
(yes I know that is not valid C but it serves this example and compiles fine :-) make CFLAGS="-g -O0" simple
(This also works with many other build tools like CMake or configure scripts generated by autoconf.)If you're curious which rules exist by default, try running:
make -p
(This produces a lot of output; a bit much to study in detail, but still useful to scan just to get an idea of what default rules exist.)Of course this only applies to GNU make.
If you are going to use C, then at least know what it's doing!
If you're going to learn C, you better learn assembly, so you know what the C is doing.
If you're going to learn assembly, you better learn CPU architecture, so you know what the assembly is doing.
If you're going to learn CPU architecture, you better learn digital logic, so you know what the adders and multiplexers are doing.
If you're going to learn digital logic, you better learn solid-state physics...
I mean, it's always useful to know what's happening under the hood, but it's not always realistic to expect that to happen. I don't think the average C programmer need to know assembly any more than the average Ruby programmer needs to know C.
Sure, you can generally get by without low level knowledge, but it's nice knowing why certain design decisions were made. And as far as C goes, I think it's extremely useful to be familiar with how it translates to assembly, at least in general. It is knowledge that you will use regularly if you're programming in C in any significant capacity.
For example, reasoning about ABIs is something you'll have to do if you ever write a stable library interface. Reasoning about ABIs requires you to know the asm-level calling convention for the platform you're coding on.
Writing C without knowing basic assembly is writing code you can't debug. I'd say that's clearly not acceptable.
If we put those aside, then the simplest example is probably when someone else's code crashes due to your input (though the bug may be theirs), and you don't have their source code. I've spent a great deal of quality time tracking down bugs in vendor libraries for which I had no source, and then working around them.
There are a ton of other possibilities, though. For instance, ordering issues related to thread-safety, out-of-order execution, and synchronization points/barriers.
Or, hell, just being able to read a failure quickly, whether or not you have debug symbols handy. Contrived example:
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x0000000000000000
0x0000000100000f25 in main ()
(gdb) disassemble
Dump of assembler code for function main:
...
0x0000000100000f25 <main+21>: movb $0xff,(%rax)
...
End of assembler dump.
(gdb) info reg rax
rax 0x0 0Even if you are having problems interfacing with a third party library, most of this stuff is pretty Googleable.
I think a good summary of my opinion would be that if you're going to primarily write C professionally throughout your career, it's best to understand fundamentals of assembly, but doing projects here and there? I don't think it's worth the time; you just won't use it that much.
Memory alignment in C is most definitely not abstracted from the programmer. The compiler does add some padding to fit alignments but they can either work for you or against you. 98% of the time it works the way you want it but when it goes wrong, it's going to be a hard problem to debug. So it's absolutely critical for a C programmer to know a thing or two about alignment, especially the cases where the compiler attempts to fix it for you.
Examples of alignments that can have a performance effect or in some scenarios (multithreading, kernel programming) may affect the correctness of the program: 4 bytes (word size), 8 bytes (64b pointer size), 16 bytes (SSE/NEON SIMD register size), 32 bytes (ARM cache line size, AVX SIMD register), 128 bytes (ARM and x86 cache line size), 4k (ARM and x86 page size). Add relevant figures for any other target archs you're dealing with.
When doing GPU work (using glMapBufferRange or something), there are other alignments to care about and they may be GPU-specific. Aligning everything to 2^N can never hurt.
Let's say for example you're trying to load a binary file and the header format is like this:
16 bit magic number, 8 bit file size, 3 bit version, 5 bit header size
Now, are you confident enough in how structs are laid out in memory that you could use a bitfield to read these values in? Are you sure it would work on 32 bit, 64 bit, big and little endian machines? What is all that packed structure nonsense in GCC?
That's something that C abstracts away from you and can lead to errors.
(I'm a C programmer, I don't look at assembly that often at all, but it's certainly helpful to be able to do so and my knowledge of the x86 ISA, calling conventions, etc has informed many decisions I've made in the past, especially re: performance)
Knowing x86 assembly is more than enough to reason backwards from assembly to the C that generated it.
I disagree. Given that C exposes you to some hardware details that are abstracted when developing in some higher level language such as Ruby, for instance having to deal with overflow on signed/unsigned types and register variables (I know that compilers will mostly ignore that, but it's still a language feature).
The average Ruby programmer, on it's turn, can perfectly live without knowing C programming. A more accurate analogy would be to a Ruby programmer to know about the language implementation being using (MRI, JRuby, Rubinius, etc.)
I really don't have an issue with someone being a dev in Ruby who doesn't understand pointers or how if/else statements can be constructed from jmp primatives. But if you're on the road to expertise I don't think you can remain ignorant of these.
Having a broad background is useful even if it's considered "architecture". Knowledge of networks and protocols is essential. Having IT chops is a great toolkit to draw from. A DBA will need to know more about storage than most developers. Understanding some RF basics helps when dealing with wireless. I've probably used more of my EE background dealing with lower levels of the stack than my CS background.
Concerning architecture: One might argue it's not neccessary but I think a good C programmer should at least have scratched the surface here. No deep understanding, but a bit of an overview of what's going on. That is - I think - the point where it no longer concerns you as a programmer. Architecture and Assembly really go hand in hand, since every architecture comes with its own assembly code (yes I know, some assemblers are abstracting that away sometimes). That is why some things work faster on, say SPARC than on x86. It also helps understand the limits we're given and what "64 bit" really means.
Understanding architecture doesn't really help writing better C code, but it explains things the compiler does, since in a pipelined architecture, the compiler will arrange assembly instructions, so there are as little stalls as possible and therefore gaining performance.
[1] I might be wrong on that though. Please correct me if I am.
And, FWIW, when I did CompSci back in the '80s, the first courses/tutorials involved building half-adders and flip-flops from NAND gates and wires on a plug-board.
You don't _need_ to know that sort of stuff, especially not in any level of detail, to build another CRUD webapp - and with luck a hugely successful business on the back of your CRUD app - but I think there's a personality trait that comes along with all the other stuff that makes some people "hackers" which characterises many of us as insanely curious. Many of us don't want to treat the latest Ruby ORM features or Node.js's async callbacks as a "black box", we want to "know how it works" - sometimes out of pure curiosity, and sometimes because it fails _hard_ and we can't work out why without understanding what it's doing "under the hood".
I think that my knowledge of architecture has influenced my C skills more than my assembly knowledge.
Also, I went through some low-to-mid range math in school (calc-3, diff. eq., linear alg.) and those classes very much made me a better programmer. The analytic thought processes you learn through those classes is invaluable.
I think it's useful to have a basic understanding of assembly so that you can debug better, or understand why some behaviors are undefined. I think that only takes a basic understanding of assembly though.
If you want to be an expert at assembly, you probably want to go deeper, but most people don't have that goal.
A master C programmer knows assembly well, a good chunk about CPU architecture, some things about digital logic and superficially about the physics.
How far you go down the rabbit hole depends of how much of this supporting knowledge you need to work at the level you work. Given the layers of software abstraction that exist in systems these days is it really necessary for a programmer to have a detailed knowledge of how a transistor works? Not at all. Your much better spending the time building a mental model of how your VM or OS layer is working and going to interpret the code you write. Or even looking up and understanding the layers that will use what you write - like users and customers (now there is a real challenge!) For sure take an interest in all things, but as you go down the layers your thinking can be increasingly abstract and like any model, plain wrong and it won't really matter.
When things go wrong it is useful to have knowledge of n-steps deep to immediately cut through ton of output and nail down a few places things seem to work not as expected.
-Carl Sagan
You can also use `objdump` from binutils
One good combination would be Assembly + C + Python for instance. Assembly helps understanding C and C helps with understanding Python.
In my CIS240 class (currently enrolled) we learn computers from the ground up and eventually learn C. First we do binary arithmetic by hand to get a feel for it, then we design basic circuits (and some complicated) that can perform the basic computer instructions (ADD. MULT, SHIFT, AND, XOR, etc.) then we keep building up off of these basics until we are finally writing C, at which point we will (hopefully) have a good grasp at what is going on and appreciate our programming language of choice a whole lot more.
In regards to the original post, I'm not sure I would be putting C with assembly or learn one over or for the other. They both have their uses and reasons for knowing. Forcing yourself to learn one before the other does not seem like a logical way to go. I think it's best to start with what makes the most sense for your end goal and / or you are most interested and motivated to begin and get most deeply into.