3

How does assembly relate to machine/binary code.

For example here is how to print to the screen in mikeOS(a small pure assembly OS), mikeOS it uses NASM to assemble.

BITS 16
    ORG 32768
    %INCLUDE 'mikedev.inc'

start:
    mov si, mystring
    call os_print_string

    call os_wait_for_key

    ret

    mystring db 'My first MikeOS program!', 0

Where os_print_string and os_wait_for_key are defined as

os_print_string     equ 0003h   ; SI = zero-terminated string 

and

os_wait_for_key     equ 0012h   ; Returns AL = key pressed

in mikedev.inc respectively and defined as

os_call_vectors:
    jmp os_print_string     ; 0003h

in kernal.asm

Now nasm must do a lot more work under the scene when assembling, I have no idea what.

In other words assembly language is a wrapper to some degree to machine code just as say C is a wrapper to assembly. If I said cout >> "Hello World" for example in C++, this is then compiled into it's assembly equivalent and them assembled into machine code.

So I am trying to understand out how 0003h and 0012h seem to dictate everything that is going on when printing to the screen. How do these two values,

a) Tell the CPU/PC system which bus to send the corresponding bytes that represent the required string to the monitor bus and not say to the sound card.

b) In this case the string is sent to the monitor, obviously, now my understanding is that you have a frame buffer that can store a maximum number of bytes. So say the resolution of your screen is set at 1024 x 768 which is 786432 pixels and a refresh rate of 60hz on the screen, therefore the FB will contain this number of byte values and hence will be sending this many bytes to the monitor every 1/60 sec. The first byte in the FB corresponding to the first pixel on the screen and the last to the last on the screen etc.

So how does the CPU/GPU know which byte to put in which position in the FB. It's like saying to the GPU 'ok I need this pixel at coord (245,232) green so I will leave it to you to put this pixels value in the correct position in the FB' etc.

How does this work.

1

4 Answers 4

4

Assembly language translates almost directly to machine code. mov becomes a mov instruction. call becomes a call instruction. The arguments on the same line become the argument fields for those instructions. There's a bit of assistance in computing addresses, but not a lot beyond that.

The operating system can be treated much like a subroutine library. The "magic numbers" you're asking about are operating system entry points; the call instruction, like a function call in higher level languages, invokes them; they run until they return, at which point your program picks up where it left off. Your OS's user manual will tell you which entry point to invoke to do what, how to set up any arguments required (such as putting the address of the string to be printed in the si register before calling os_print_string, though some may involve pushing values onto the stack rather than putting them in registers), and how to read their returned results if any (again, which registers will have the result or what to pop off the stack).

As far as question (b) goes -- That's all stuff the OS and its device drivers, or a function library linked with your assembly code, will normally handle for you. If you really need to know it (eg because you're writing an OS or device drivers), you'll need to study the documentation for your specific hardware to understand how to communicate with it... but what you'll wind up doing is writing a library of functions which do the necessary work, and packaging it so main programs just invoke those functions. In other words, for most programs I/O is much like working in a higher-level language; the runtime library does all the work and all you need to know is how to use it. (Sane assembler code is critically dependent upon writing good functions so you don't spend time endlessly reinventing wheels!)

3

In respect to what those os_print_string = 0003h and other "magic numbers" are, they're entry point addresses, not to the actual subroutine, but to a jump table which in turn jumps to the actual location of the OS subroutine.

When I first started learning assembly language it was on a system which did not have much organization in the ROM layout. If you wanted to make use of the ROM routines you had to set up the CPU registers and directly CALL the address of the routine.

All well and good until the ROM is updated and all the addresses change...luckily, the ROM in question never did get any updates which would break those calls. If it had, a great many software titles would have stopped working.

Jump tables solve this problem by ensuring that the published entry point for an OS routine never changes, even if the machine code itself moves around between different OS versions. In this case the jump table entries are at 3 byte increments so the table will look a lot like:

0000h    JP someaddr           C3 LL HH
0003h    JP print_string       C3 LL HH
0006h    JP yetanotheraddr     C3 LL HH
0009h    JP yetanotheraddr     C3 LL HH
000Ch    JP yetanotheraddr     C3 LL HH
000Fh    JP yetanotheraddr     C3 LL HH
0012h    JP wait_key           C3 LL HH
...
...

LL and HH may be switched depending on the endian-ness of the CPU, C3 happens to be the opcode for JP on a Z80.

No RET is needed between these because the JP does NOT save the current execution address on a stack (whereas the CALL which got you there does) so the last RET in the OS routine will cause execution to continue right after your call to the jump table.

As to how the system knows which addresses in the framebuffer to manipulate, that's all taken care of by os_print_string (and whatever other display related OS routines are available to you; there may well be something like os_set_point). The framebuffer address, hardware, implementation etc can all change, but your code only cares that there is an OS routine which prints characters to the display (or sets points, or draws lines, or...)

3

Paul Glover correctly explained the working of those system call values as being addresses that contain jump instructions to the actual OS routines.

That is to say, the CPU/GPU do NOT know where to put your pixels, but the OS routines, or drivers, do. You pass them arguments (x, y, r, g, b), and the OS routines put the r,g,b values to the byte positions correct for the buffer in use. If eg. you have a 10x10 pixel screen, its buffer is an array of 10x10x3 = 300 color intensities.

If each of them is a byte, setting pixel (3,5) to yellow might mean setting buffer[3*(3*10+5)] to 0xFF (fully red), buffer[3*(3*10+5)+1] to 0xFF (fully green), and buffer[3*(3*10+5)+2] to 0x00 (not at all blue). The screen itself (ie. it's controller) reads the values from the buffer and sets the individual color dots to the specified intensities.

If there is a GPU, the process may be different in that the OS routines put 'blueprints' into memory, tell the GPU to display them (passing the address, or placing them at a fixed address for that purpose), upon which the GPU calculates the pixels and places them in the buffer.

buffer[n] is just a shortcut for memory[buffer_start + n], where memory may be on the same bus as the RAM, or it may be on a designated I/O bus.

The asm instructions themselves are also numbers, and their 'arguments' or 'operands' are numbers too.

When trying to understand how these machine-level (as opposed to OS level) mechanisms work, some analogy to the physical world might help.

Suppose you have a car. There are three pedals, a lever for selecting a gear, and a steering wheel. Suppose that pedals can only be up or down, there are a maximum of 7 gears, and 4 states of the steering wheel. That setup would give you an 8-bit car ;)

Now the engineer who builds that car gets to decide about how to wire it. My proposal would be:

    Wheel                 Pedals                Gears
0 0  locked         0 0 0  roll (nop)       0 0 0  no gear
1 1  straight       1 0 0  change gear      1 1 1  7 = reverse
1 0  left           0 1 0  brake            0 0 1  1.
0 1  right          0 0 1  accelerate       0 1 0 ... 1 1 0   2., ..., 6.

So, 'accelerate while turning left in 2nd gear' would have an assembled opcode of

1 0  0 0 1  0 1 0 == 0x8A
ie. (wheel << 6) | (pedals << 3) | (gears) = encoded instruction.

Any opcodes having more than one pedal bit set would be invalid, except for the combination of 'change gear' and 'brake'.

Or maybe, the gear is only an operand when actually changing gears, and the opcode for simply 'accelerate while turning left in current gear' is 0x88. Or maybe, the engineer is not me (I'm not), the bits are placed differently, and diffferent opcodes result.

The point is, the engineer decides, and once it's built, the bits are 'wired' to have precise meaning, and the way they are loaded specifies the manner in which they must be chained to produce numbers that are valid instructions.

You should think of the bits as switches, or the user interface of the CPU itself. Quite similar to a church organ, where you have levers that determine which sets of pipes get the air pressure applied through playing notes on the keyboard.

As to the relation of assembler text and actual machine code:

  • each text representation of an instruction usually has one single way to be encoded.
  • each encoded instruction can have an arbitrary number of ways to write it textually, such as for JNE and JNZ, which both check the Zero flag and jump if it's not set (and produce the same encoded instruction), but represent different reasons to do it (previous operation result != 0 vs. previous comparison non-equal) as a way to simplify understanding the source code for humans.
  • There may be valid machine instructions that have no text representation at all.

If you want to fully understand instruction encoding for any given platform, there's no way around the instruction reference.

2

So ideally assembly language has a one to one relationship with machine code. One line or mnemonic (add, sub, xor...) goes with one machine instruction (add, sub, xor...).

Then to make programming easier, they add macros, so just like macros in other languages you can save some typing.

Likewise the instruction set lets you make function calls, the syntax is of course specific to your architecture and assembler, so you can save some typing there just like in any other language.

Then you have system calls, and unfortunately there is too much reliance on system calls when learning "assembly language". Learning system calls is I would argue something you do after you learn assembly language. The C equivalent is learning C then learning the C library functions, rather than learning "some" C while learning the C library functions. System calls, which on x86 boil down into int this or int that (10h, 21h, etc). Basically trigger some other code written typically by someone else which is the operating system or bios or whatever you want to call it. And there is more code there programmed in some language, compiled and/or assembled. For example there is likely piles of code behind your simple print string call. However you call a system call function is defined by that operating system you are living on top of, in this case it appears you have the magic numbers you are asking about that are specific to and defined by that operating system/environment you are running on top of. You would need to get the documentation for that environment/system and then learn what registers, etc need to be set to what and how to make the call. This would be true for any assembler. If the assembler has its own environment then you use the assemblers definitions for its system calls and simply do what it says. Just like memset in C, it is well defined somewhere you need to create a function call that has the right parameters in the right place as defined by the documentation for that function, compile and link it right, run it in the right place and that function will happen as documented and return as documented.

1
  • "operating system or bios or whatever you want to call it"? I think being specific here is important. Maybe discuss the difference between hardware interrupts and OS calls. There is a difference and they are both useful.
    – user22815
    Commented Apr 21, 2014 at 13:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.