11 ms·
How to Read Assembly Language
- nayuki 6y agoMy take on learning assembly: https://www.nayuki.io/page/a-fundamental-introduction-to-x86-assembly-programming https://www.nayuki.io/page/a-fundamental-introduction-to-x86...
- dragosmocrii 6y agoThank you for this!
- Stratoscope 6y ago> Note that my tutorial uses the AT&T assembly language syntax instead of Intel syntax. This makes me curious: how widely used is the AT&T syntax these days? I could easily be wrong, but my impression was that most work is done in the Intel syntax. This may be ignorance on my part, as the Intel syntax is all I've ever used on Intel processors. I've coded in a dozen other assembly languages for various processors, but Intel chips seem to be the only ones with two different assembly language syntaxes, right down to the source/destination order.
- muststopmyths 6y agogdb and lldb's default disassembly syntax is AT&T, so I'd say it's pretty widespread.
- lokedhs 6y agoThe M68k has two different syntaxes too. The argument order is still the same (src,dest, the correct order in my opinion), but other things are different, with the main difference being indirect addressing which is (a0) in Motorola syntax, while it's a0@ in AT&T syntax. So while the changes might not be as huge, it's definitely not just Intel.
- masklinn 6y ago> The argument order is still the same (src,dest, the correct order in my opinion) OTOH dest, src matches every assignment ever, and is much more regular when extending to multi-parameter mnemonics (e.g. madd).
- colejohnson66 6y agoI’m going to ignore the characters such as % and $, and also the parameter order (sortove) There’s also the fact that the Intel and AMD x86 manuals list them as MNEMONIC dst, src1 GAS does this: MNEMONIC src1, dst Which is fine, but what about 3 and 4 operand instructions? I always have to look that up. 8087 FPU instructions also (in GAS), due to a historical quirk, are in “Intel” form: MNEMONIC dst, src Then there’s that whole indirect syntax of: segment:displacement(base,index,scale) Whereas “Intel” syntax is much clearer: segment:[base + index*scale + displacement] This leads to weird things like: [ebx*2+10] ; I can see what that’s doing 10(,%ebx,2) ; I need to remember what the order is and convert it in my head I’m not trying to convince anyone here. If you like “AT&T” syntax, that’s fine with me. I just prefer “Intel” syntax because of these reasons.
- retrac 6y agoIt's still the same division as always. Linux and other UNIXes on x86 and x86_64 generally use AT&T syntax. The rest of the x86 world uses Intel syntax. And of course *nix has always been a minority on x86. > I've coded in a dozen other assembly languages for various processors, but Intel chips seem to be the only ones with two different assembly language syntaxes, It's a historical quirk. Generally, today if there is a widely used and accepted assembler syntax targeting the architecture, UNIX porters would use that assembler's syntax. GAS accepts normal ARM style syntax, for example. But that's today. x86 is ancient. And so is UNIX on the x86. Going all the way back to the late 1970s, the 8086 was one of the first microprocessors that could host a decent UNIX environment. The rush was on as soon as the chip was released. (Most of the porting fervour would shift to the 68000 about 18 months later when that came out.) There was no established assembler aside from the one from Intel. MASM wouldn't be written for another couple years. UNIX at this point already had a full quasi-portable toolchain, which of course was familiar to the porters. And they planned to live in an all-UNIX world anyway. The only assembly anyone would ever write would, hopefully, be a dozen lines in the kernel. So they just adapted what they had and what worked. The old PDP-11 UNIX assembler, backwards syntax and prefixes and all. And here we are 40 years later.
- jart 6y agoAT&T syntax is best syntax. Linux, GCC, LLVM, etc. all use it. Scott Wolchok writes it in a style that's more verbose than it needs to be. It's probably a good call since this is an educational blog post. Like there's some nuance to when you need things like q on statements `movq %rdi,8(%rsp)`. For example, this is fine: `mov %rdi,8(%rsp)` but you need it if you move an immediate to memory, e.g. `movq $123,8(%rsp)`. I like to avoid them because mnemonics like `orb` don't appear to the untrained eye as the `or` instruction! The GNU-style suffixes can also appear multiple times within an instruction name in various combinations, e.g. pclmullqlqdq vs. pclmulhqhqdq.
- swolchok 6y ago> Scott Wolchok writes it I didn’t write it, I just copied the compiler output from Godbolt so that the article could be read on mobile, where Godbolt’s UI doesn’t work so well and switching tabs back and forth is difficult.
- unwind 6y agoCool! A minor typo if the author is here: the first example shows 2D integer vector code, but the declaration has the line int64_t z;
- swolchok 6y agoThanks! Fix should deploy momentarily. This is what I get for duplicating the examples across Godbolt and the post itself. It was originally 3D for no particular reason and I cut it to keep things short.
- 29athrowaway 6y agoOne thing is reading assembly language written by a human, another thing is reading assembly language generated by a compiler. Compilers are insane, especially when you turn on optimizations. Another thing is that there is more than 1 asm syntax. Popular ones are the Intel syntax and the GNU assembly syntax. The way you read those is different. If you want to get started in a simple way with little friction I suggest you do this. 1. Go to godbolt.org (Compiler Explorer) 2. Write a simple function (not program, that will complicate things), such as one with no parameters that adds two hardcoded. Like this int sum() { int result = 0; result += 1; return result; } 3. Compile with with no optimizations (-O0). 4. Read the output. Note that the colors represent each statement. Pay attention to that. Also, by hovering over an instruction for a few moments you can get a definition of what it does. Then you can start making adjustments such as using parameters, adding control flow like if statements, invoking other functions, using float instead of int, etc. Another thing are calling conventions. Functions do not exist in assembly, they're an abstraction. There are many ways to represent a function call and these are some of them: https://en.wikipedia.org/wiki/X86_calling_conventions https://en.wikipedia.org/wiki/X86_calling_conventions
- mhh__ 6y ago> compilers are insane Compile factorial with avx enabled and the compiler will often go mad and shit out a 150 instruction body when n=low overflows and thus invokes undefined behaviour anyway
- tralarpa 6y ago(Edit: I just noticed (yes, I am slow) that you were probably referring to code written by a human for educational purposes. Sorry for the misunderstanding) > One thing is reading assembly language written by a human, another thing is reading assembly language generated by a compiler. Compilers are insane, especially when you turn on optimizations. In my experience, it's the opposite. Compiler-generated code has a very homogeneous global structure that is easy to understand even if the compiler does complex stuff on instruction level. Human-written code does things that force me to be permanently on my guard concerning the control flow and the meaning of data, and I am not even talking about code (like malware) designed intentionally to be confusing. Examples from computer games: - Calling a function X and directly wanting to jump to another function Y afterwards by first pushing the address of Y on the stack, then call X. Sometimes combined with a jump into the middle of another function. - Manipulating stack frames, accessing local variables of the caller function, etc. - Using the same location in memory to store completely different data types (imagine a C program using unions everywhere, even for a simple counter variable) - Organising data on bit level - Something extremely popular in older games: Collection types with non-uniform elements. Imagine a kind of array or list where some elements are 4 bytes long and some are 6 bytes.
- 082349872349872 6y agoA way to squint at assembly language: as backwards functional code where almost all subexpressions have been recursively extracted out into local definitions (except for leaf un- and binops) ... and then the resulting temporaries reused.
- wruza 6y agoAnd then in a CPU “runtime” that reuse is un-reused and reordered for a better superscalar pipeline throughput.
- pjmlp 6y agoFor any compiled language you can read their generated Assembly via online tools like https://godbolt.org/ https://godbolt.org/ For .NET languages use https://sharplab.io/ https://sharplab.io/ For Java, https://github.com/AdoptOpenJDK/jitwatch https://github.com/AdoptOpenJDK/jitwatch For JavaScript, if using V8 there are the set --print-something flags.
- vardump 6y agoIt's eye opening how most trivial functionally equivalent code generates practically identical native binary output regardless of the language, be it C, Java, Javascript or .NET.
- pjc50 6y agoWhy is that eye-opening? That things might converge on the same optimal result?
- vardump 6y agoIt's eye opening, because it shows how small the actual differences in executed code really are. When I first tried this about 10 years ago, I was very surprised how good code Javascript JITs generated. I expected there to be type checks, lots of unnecessary memory accesses, etc. all over, but to my surprise the inner loop was practically same as gcc -O2 output when compiling an equivalent C program. Of course the same is not true when it comes to bigger blocks of code, because Javascript does need to do a lot of bookkeeping behind the scenes.
- pjmlp 6y agoI guess it is more like that many are still unaware of it when mixing languages with implementations, while arguing language X vs Y.
- _448 6y agoA brilliant insight on these similar lines was exploited by a very brilliant engineer(https://news.ycombinator.com/user?id=jart https://news.ycombinator.com/user?id=jart) to go even deeper and generate single cross-platform binary of a program (https://news.ycombinator.com/item?id=26271117 https://news.ycombinator.com/item?id=26271117)! This magic is achieved using this: https://news.ycombinator.com/item?id=25952540 https://news.ycombinator.com/item?id=25952540
- Voltage 6y agoIn Example 3, there is a 'pushq %rax' but no subsequent pop command. Error or is the stack being manipulated in another way elsewhere?
- deleted 6y ago[deleted]
- wruza 6y agolea rsp, [rbp-24] sets stack pointer to 3 registers above saved rbp. (or below, depending on the viewpoint)
- dominicjj 6y agoThis bothered me as well. The only thing I can think of is that one of the calls to libc functions is consuming rax off the stack. The code as displayed by GodBolt is the code as generated by clang so it's not an error there. EDIT: I've just noticed this line: leaq -24(%rbp), %rsp It does a manual pop of rax by restoring rsp to what it was before the prologue.
- JdeBP 6y agoWhat is going on is this. A function activation record contains various parts. * http://jdebp.uk./FGA/function-perilogues.html http://jdebp.uk./FGA/function-perilogues.html The calls to push and pop R15, R14, and RBX in the perilogue are manipulating the save area. The space below the save area is the locals area, and there are local variables in the function. The inner save and restore of the stack pointer in R15 is adding further, block-scoped, locals to the locals area for the duration of that block, which is why it hasn't been combined with the outer perilogue (one of the things asked about in a footnote). If this function were more complex, the restoration of the original stack pointer from R15 would be well before the function epilogue, demonstrating the disconnect more clearly. The actual epilogue begins with moving the stack pointer back up to the bottom of the save area, from whereever it happens to have been after the locals area was allocated, so that the save area can then be popped. In the absence of alloca() and variable-length arrays, this could be done by ADDing to the stack pointer register, as the size of the locals area is known by the compiler and fixed at compile time. alloca() and variable-length arrays make things more complex, though, and this function contains at least one of those. Getting the amount to ADD right would involve retaining the sizes of relevant alloca()s and variable-length arrays. (Also, compilers prefer LEA to ADD for the stack pointer, but for the sake of simplicitly let's just treat this as ADD.) Fortunately, the address of the bottom of the save area is known, and working out how much to ADD is thus unnecessary and the problem is moot. The bottom of the save area is by its nature at a fixed offset from the frame pointer, with a size known from which registers had to be saved, so the generated code just resets the stack pointer to that offset relative to the frame pointer, effectively popping off the entire locals area that way. There's no reason to POP what was pushed from RAX back into the register, because that part of the stack is not part of the save area. It's a dummy local variable that ensures that the stack remains 16-byte aligned once all fourof return address, saved frame pointers area, save area, and locals area are in place. As another footnote notes, PUSH RAX is just a quick way of subtracting from the stack pointer without using the ALU, which a SUB instruction would. RAX isn't actually being saved.
- ChrisMarshallNY 6y agoGodbolt is a fun tool: https://godbolt.org/ https://godbolt.org/ The story I heard, was that the author is one of these obsessive genius types, who was having a debate about compiler efficiency, so he wrote an entire compiler engine to prove his point. I can understand that. This is a great article. Being able to descend into machine code/assembly is a valuable “tool of last resort.” Personally, I avoid that like the plague, but an OS developer probably still needs to do this (I developed system stuff, back in the Dawn Times). I started off with Machine Code, so it’s been a long, strange trip, for me...
- flohofwoe 6y agoIMHO you don't have to be obsessive to use godbolt.org in your daily work, neither should it be a "tool of last resort". It really does help a lot to understand what the compiler will actually turn your high level code into, because quite often the results are very surprising (or if only to be shocked about the amount of assembly code that's generated from a few lines of C++ template code). Comparing the differences in output between different compilers and optimization levels is also very useful.
- ChrisMarshallNY 6y agoI consider Godbolt to be an educational and “thinking cap” tool, as opposed to a critical path optimization tool. It is pretty damn cool, though. It’s sort of like how an algebraic graphing app is most useful for students, as opposed to working engineers. Nowadays, with on-chip threading, ASM stack dumps are a lot less useful than they used to be. Really, for me, munged function names are what I use to figure out the general vicinity of a bug, which I then figure out, by examining the source.
- amelius 6y agoI don't understand this tool. My compiler has a flag for generating assembly output. That should be sufficient, no? Also, it more accurately reflects what my compiler actually generates given all the other flags.
- 6y ago
- Hnrobert42 6y agoWhy does assembly use cryptic instruction and variable names? imulq could be integer_multiply_64bit. Everyone has big monitors these days. There isn’t space to save. Is it just historical inertia and/or “it’s not so bad. if you are smart like me, it’s easy”? Or are there good reasons to keep things so terse?
- oilbagz 6y agoThe answer: Decades and decades of tooling. You wanna go refactor all that code to make it more verbose?
- skissane 6y agoThe good reason for this terseness is that assembly is inherently verbose. A single line of code in a high-level language can easily become a dozen in assembly. Your proposal would be taking an already verbose language and making it even more verbose. “imulq” is not just less typing than “integer_multiply_64bit”, it is less reading too.
- rectang 6y agothissentenceistersebutitisstillhardtoparse.
- wruza 6y agoIf you are annoyed by something innocent like i-mul-q, then check SSE and AVX. Btw, intel syntax doesn’t have these [bdq] suffixes (uses operand typing instead) and looks more clean for simple commands like mov, mul, add, and, etc. Personally, I don’t see any reason for them to be too wordy. Upd: it is also easier to read when operands are aligned on the same column, because values move between registers form line to line.
- rayiner 6y agoThere is a fixed set of instructions so anyone with a bit of experience will know what they mean. It’s different than being verbose with variable names that can be different in different programs.
- 6y ago
- _448 6y agoAny good resource to learn Arm Assembly?
- oilbagz 6y agoI found this pretty useful recently: https://azeria-labs.com/writing-arm-assembly-part-1/ https://azeria-labs.com/writing-arm-assembly-part-1/ There's also this, but I haven't dug much into it yet: https://hackaday.com/2018/09/20/learn-arm-assembly-with-the-raspberry-pi/ https://hackaday.com/2018/09/20/learn-arm-assembly-with-the-...
- swolchok 6y agoI’d be happy to port the article to ARM64 if there was sufficient demand for that, but I think it’s still fairly niche among developers, right? (Apple, recent Android, niche server environments?) Are you talking about ARM64 or ARMv7?
- retrac 6y agoA subset of 32-bit ARM is quite common these days in education, often replacing where MIPS was once used. ARM64 has really swept in the last couple years. My two year old budget Android smartphone has an ARM64. So does my Raspberry Pi. Lots of hardware hacker types likely to play around with assembly would be using ARM, and often 64 bit these days. That said, it is relatively niche. A lot of those ARM64 machines are running 32 bit software still. But I do expect it to be the dominant architecture in just a couple years.
- _448 6y ago> Are you talking about ARM64 or ARMv7? Either will do. The goal is to learn :)
- swolchok 6y agoI ported the article to ARM64. https://wolchok.org/posts/how-to-read-arm64-assembly-language/ https://wolchok.org/posts/how-to-read-arm64-assembly-languag...
- chmaynard 6y agoNote to author: Because this is a personal blog, your RSS feed might as well include the entire post instead of the first few sentences. Just a suggestion.
- Grimm1 6y agoBack years ago when I had much much more freetime I binged the below to learn how to read x86 assembly and use things like gdb. It's a lot of information to take in but I was getting into reverse engineering back then and have a bit of an obsessive personality so I thought it was great. Still do, so I refer people to the resources anytime something like this pops up. https://opensecuritytraining.info/IntroX86.html https://opensecuritytraining.info/IntroX86.html
- vsundar 6y agoThis looks very useful. Thank you for this.
- gtm1260 6y agoAhh I remember sweating bullets, trying to diffuse assembly bombs for labs in college. I love how looking at GDB starts out like literally staring at heiroglyphics but by the end of the semester, you get such an intuitive sense for things. Haven't really 'used' that knowledge directly, and I don't think I will. But it taught me a ton about how computers work, which probably helps me somehow!
- tptacek 6y agoThe major breakthrough I had in being able to comprehend assembly code was when I learned to look at it in basic blocks, rather than as a straight line. I don't know if that's super obvious (it seems obvious, written out that way), but just taking a listing and adding newlines at block boundaries is massively helpful to me.
- chrisaycock 6y agoThat's definitely how a compiler does it, especially at the IR stage. It's much easier to track values within a localized region; this applies to both people and computers.
- tralarpa 6y ago> it seems obvious, written out that way Yep. Most disassembly tools have configuration settings specifically for that (add newline after jumps, put labels on separate lines, etc.)
- tptacek 6y agoSure, and I'm guessing I learned to read assembly this way from IDA Pro; I'd had to deal with assembly long before IDA was a thing, but I was never an especially able reader of other people's assembly. But even today, like, when the eBPF verifier coughs on a hairball, I end up pulling assembly into Emacs and then doing a quick pass marking up the basic blocks before I try to read it.
- dtornabene 6y agoI'm sure its not too much trouble to whip up on my own but if you have any emacs lisp helping you do this I'd love to read (and use) it
- tptacek 6y agoI wish. If you write it, let me know.
- 6y ago
- waynesonfire 6y agoI've heard of people writing custom assembly to get more performance... what level of assembly expertise is required to achieve that? And, what does that look like? Do you let the compiler emit assembly code and then start editing it to make it faster? Or, does it start from scratch? Are there known areas that the compiler has troubles with?
- chrisseaton 6y agoIt's almost never 'assembly expertise' - it's architecture expertise - knowing a specific instruction you want to use in a specific way to get some kind of fusion or fill some functional unit using some knowledge that the compiler doesn't have. The actual mechanics of how to write a program in assembly are trivial if you already know a language like C.
- retrac 6y ago> trivial if you already know a language like C The few times I've been stuck writing code for a platform without access to a compiler, I just wrote it in pseudo-C then hand-compiled it. Once I got comfortable I could "compile" on full mental autopilot, checked out and watching TV at the same time. But it's horribly dull work, like doing arithmetic by hand.
- titzer 6y agoThe perennial problem for me is the bifurcation from "AT&T" and "Intel" syntax. I don't really care if register names have a $, a %, or no prefix at all. It's the reversal of source and destination operands that is absolutely maddening. Personally I prefer that destination operands are on the left, because that tracks with assignments in normal programming languages. I don't really understand why you would want the opposite.
- pjc50 6y ago"MOV A, B" reads more naturally as "move A to B". I agree that we should just pick a convention!
- root 6y agoCOBOL has MOVE ... TO ... which seems to be the logical order for a command named MOV/MOVE.
- gumby 6y agoAT&T simply adapted the assembly syntax of earlier machines, ultimately which reflected the architectures of even earlier hardware from a time when there weren't really higher level languages. Some bizarre high level conventions, like using '=' for assignment, were also adopted from earlier languages but are now commonplace.
- patrec 6y agoIt becomes more natural/C-like if you think of ADD foo bar not as foo + bar but as foo += bar And then apply a prefix syntax correction, to end up with += foo bar (Or, equivalently, always mentally add the word "TO" to each operator that modifies an argument, which is most) Also, most intel instructions use one or two arguments as input, and modify one of them as output. It sort of makes sense to have the output (or input/output) argument always be in the same position, regardless of whether an instruction takes 1 or 2 args. BTW, even in riscv where "add" takes three args, i.e. the form is a = b + c, the output-only argument a comes first.
- titzer 6y ago