Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Locked thread
rjmccall
Sep 7, 2007

no worries friend
Fun Shoe
Macros in assembly programming are kindof like functions in higher-level programming: they can be a nice way to avoid repeating common code, but it's not like making everything a function solves all your problems for you.

I would actually caution you away from starting with a fake/simplified/toy instruction set. First, the major instruction sets are actually not massively more complicated in how they do simple tasks; at worst, you will see instruction operands that involve accessing memory, rather than having that split into a separate instruction. More importantly, though, working with a real instruction set gets you closer to the thing that's actually valuable about learning assembly programming, which is understanding how real hardware works.

I think the best approach is probably to write small C functions, compile them to (unoptimized) assembly, and try to understand how the result actually implements the function you wrote. Start with a function that doesn't have parameters, return values, or calls; then add those things once you understand the basics (and once you've found the Wikipedia article about calling conventions for your platform).

rjmccall fucked around with this message at 07:32 on Aug 24, 2014

Adbot
ADBOT LOVES YOU

rjmccall
Sep 7, 2007

no worries friend
Fun Shoe
The GC just happens. The design of JVM/CLR opcodes gives the VM enough type information to support a fully type-accurate GC. It's hard to answer the question with any more fidelity than that without writing a massive effort post.

rjmccall
Sep 7, 2007

no worries friend
Fun Shoe

osirisisdead posted:

Kinda. Subroutines are more like functions. Most of the macros in higher level programming that I've been looking at lately, linux kernel bootsector header stuff, are put in there to avoid the subroutine/function call overhead. Like, putting the current registers away, pulling the arguments off of the stack or whatever.

I was making a point about the role of macros in assembly programming, not saying that macros are actually technically similar to functions in the end result.

Much like functions in higher-level languages, macros in assembly are useful when there's a common operation you need to perform. Like a function, using a macro has the advantage of allowing you to implement this operation in one place, which is especially helpful when (like a lot of the assembly programs I've written and seen) the instruction sequence needs to be slightly different in different build configurations. Of course, unlike a function, there are no universal (per platform) conventions to help you separate concerns, and a well-written assembly program needs to be carefully documenting what registers a particular macro is allowed to clobber and what exactly it expects of and does to the stack; you can think of this as basically being a custom "calling convention" for every macro. And since these are macros, often these conventions are parameterized so that you can tell each invocation of the macro to use a different scratch register based on the needs of the "caller".

Macros in C are a different story. The legitimate reasons to use macros in C are when you can't do it any other way, like making min work for arbitrary argument types without C++-like function overloading, or making a function like assert that doesn't always evaluate its argument, or doing something that requires creating a new local variable in the "caller". Call overhead, however, is not a legitimate reason, for three reasons. The first is that call overhead is really not that significant anymore; modern processors are very good at executing simple call/return sequences. The second is that obviously profitable things like min will definitely be inlined if you give the compiler a chance. The third is that every respectable compiler provides some way to force a function to be inlined regardless of whether the compiler thinks it'll be profitable.

rjmccall
Sep 7, 2007

no worries friend
Fun Shoe
Cool. I was just talking about how macros are used in assembly programming because someone asked about that. I certainly didn't mean to derail you.

rjmccall
Sep 7, 2007

no worries friend
Fun Shoe

Bruegels Fuckbooks posted:

The compiler can order the allocation of local variables however it wants - it isn't forced to allocate at the point where the variable is declared, or even at all (a trivial example being not allocating an unused local) - it only needs to allocate a local the first time it is used.

In reality, outside of some special cases, compilers generally allocate all of the local variables at once: during entry to the function (the "prologue"), a stack frame is created that's large enough to fit everything they need to put there. Shrink-wrapping the stack to only fit the storage actually in use introduces a lot of extra bookkeeping: in the compiler's internals, in stack-layout metadata (e.g. for debuggers), and dynamically in stack-pointer adjustments. It's not generally considered worthwhile.

Of course, if a stack allocation is actually dynamically-sized, as a C99 variable length array is, then you don't really have a choice about it — you can't just do a single dynamically-sized stack adjustment in the prologue because, in general, you can't compute the required bounds until you get to the right part of the function.

All that said, it is a common optimization to re-use parts of the stack frame depending on what's actually simultaneously in scope. A very simple version of this would be to lay out the stack frame as if you really were dynamically pushing and popping variables individually; that pass would assign offsets to all the variables, and you'd just make a stack frame of whatever the high-water mark was. A more sophisticated version would be tied into the register allocation and use actual value liveness information.

LeftistMuslimObama posted:

Where's a good place to read up on bit-based arithmetic? Specifically I don't get the whole two's-complement thing. I saw some mentions that MIPS includes an addi op but not a subi op because you can turn addi into a subi using a two's complement operation.

Right. subi $t, $s, 4 is obviously the same as addi $t, $s, -4, so what you're really asking is how to figure out the bit pattern for -4 from the bit pattern for 4.

Unsigned addition in binary works the same way that unsigned addition in decimal does: you add the digits from right to left, possibly carrying a 1 from the previous step each time. Two's complement is just an encoding of negative numbers that lets you implement signed addition (in fact, most kinds of signed arithmetic) using exactly the same algorithm as unsigned, so that the hardware doesn't have to care which one it's doing. Machines with one's complement or sign-magnitude representations actually had to have separate add-signed and add-unsigned instructions if they wanted to implement both efficiently.

Mathematically, -x is the value that, when added to x, yields 0. In other words, if x is 01101101, then -x is the bit-pattern that, when added to x, yields 00000000. (We'll assume we're doing 8-bit math here.) If you flip all the bits in x (i.e. if you compute ~x), you get a bit-pattern that, when added to x, ends up with all the bits set: in every digit, you end up adding 0+1 or 1+0 without a carry. So that x + ~x == 11111111, regardless of what x was. If you add 1 to that, then mathematically you would get 100000000; but we're doing 8-bit math, so that top bit disappears as overflow, and you get 00000000. So (x + ~x) + 1 == 0, regardless of what x was. Because addition is associative, that means that x + (~x + 1) == 0. So -x is always ~x + 1.

  • Locked thread