There are a lot of ways to program a computer; one of them is by using assembly language, with the free NASM assembler, on the Windows platform.
One of the things I like about assembly language is that you actually get to think and program from the processor's perspective.
Materials - What Will You Need?
- The NASM assembler (NASM)
- The ALINK linker (ALINK)
- A text editor - Notepad++ (Notepad++) is a good one
Note: When you install NASM and ALINK, it is preferred to install them to the C:\ directory, for easy access.
Windows Programs - What Are PE Files?
PE files are portable executable programs, that are intended for the Windows platform.
A PE file has headers, code, data, and idata sections; there are probably more, but we'll just go over the basics here.
- The code section of the executable is the area inside the file that contains the code for the program.
- The data section encloses the data of the program.
- The idata section has the symbol import tables.
- There's also the bss section, which is also for data, but it's not part of the actual file on the disk.
It's usually the headers, then the code, then the data, within the PE file. The bss section extends off of the executable, when it is in memory.
For a reference to the PE/COFF file format, you can read the Microsoft PE and COFF Specification (The Page).
The Idata Section - So What Is This Symbol Import Table?
Every Windows program uses Windows functions. That is, when a Windows program needs to, ie, read a file from a disk, it has to contact the operating system and "ask" it to do that.
Under Windows, the method of contacting the operating system is using Windows Application Programming Interface (API) (or Win32 API), which means (basically) Windows functions. To use Win32 API, it is necessary to import the functions that we are planning to use, from the correct Dynamic Link Library (DLL).
Dynamic Link Libraries - What Are DLLs?
A DLL is a file, similar to a PE, but it's not run as an ordinary program. A DLL is essentially a library of functions.
DLLs don't have to be just with Windows, programmers can make their own DLLs for their programs too.
Functions from DLLs can either be imported at run-time, or during program start-up (the time when the program is started).
To import a DLL at run-time, the program needs to use Win32 API to load the DLL and then use Win32 API to get the memory address of the function it needs to call.
To import a DLL on program start-up, the PE file needs to have the import table entries for the desired functions.
The import tables need to include the function names, along with the names of their container DLLs.
Part of the import section is the import address table. When the Windows PE loader loads our program into memory, it replaces entry in the import address table with the memory addresses of the corresponding Win32 API functions. So when we import a symbol, we access it by referring to it as if it was a variable.
Memory - What About Computer Memory?
Just about any computer has memory. Memory is kind of like the "workspace" of a computer.
When the computer does something, it uses memory. Before writing data to a disk, you have to have the data in memory. To execute code, it needs to be loaded into memory first.
To access memory, you need pointers. Those can be any values that are (in the 32-bit world) 4 bytes long.
The operating system (Windows, in our case) restricts the use of memory of our program to a designated address space, so that our program doesn't cause trouble at important memory locations.
An address space is a range of memory addresses. Each byte, in memory, has its own unique memory address.
So if we have a 16-byte memory chip (very unrealistic, but okay for an example), and the bytes in memory are:
12, 17, 84, 244, 123, 93, 83, 194,
19, 23, 94, 38, 45, 75, 243, 95
Then the byte at memory address 7 will be 194, the byte at memory address 0 will be 12, the byte at memory address 11 will be 38.
The Address Bus - How Does The Processor Access Memory?
The processor uses the address bus to access memory. The address bus is limited in size, however, so if you try accessing a memory address that is larger than the address bus then the address will be truncated. This kind of truncation is called wrap-around.
So if you have a 2-bit address bus (again, not realistic, but fine for this example), and you try accessing the memory address 101, then you'll end up accessing the addres 01, and the high-order part of your address will be ignored.
Let's say we have a 2-bit address bus and the same memory chip from above, with the same values. What would happen if you try accessing the byte at address 12? What about address 9?
To find out the answer, we first need to get the binary version of the address.
12 to binary = 1100 (it's basically 1*(2^3) + 1*(2^2) + 0*(2^1) + 0*(2^0))
9 to binary = 1001 (1*(2^3) + 0*(2^2) + 0*(2^1) + 1*(2^0))
Since we have a 2-bit address bus, we need to truncate the addresses to form 2-bit values.
1100 to 2-bit = 00
1001 to 2-bit = 01
00 to decimal (from binary) = 0
01 to decimal = 1
So we are actually accessing bytes at physical addresses 0 and 1.
A physical address is an actual address in memory.
A virtual address is the address that we're trying to access; virtual addressing is used by a lot of operating systems, including Windows.
The Intel Architecture Software Developer's Manual volume 3 (Document) tells a lot about paging and virtual memory, if you want to read more about that.
Compiling A Program - How Is A Program Compiled?
To compile a program, a programmer usually compiles all the files that go into the project (besides the files that are included, of course). The compiler (the program) compiles the source code into object file(/s). Then you use a linker to link all the object files into one executable PE file. Though some compilers compile and link with one command.
But unless we're building a big project, one source code file is usually enough (we're not counting the includes).
Also, the linker doesn't know which code has the entry point (the point where the program execution starts), so that point must be specified. In NASM, that point can be set using the "..start:" special symbol; the program will start running from the point where the "..start:" label is located. This is usefull especially when you link more than one object files.
In assembly language, you don't compile files. What is called compiling for higher-level languages is actually called assembling for assembly language. But the other thing about it is that while compiling converts high-level statements to machine code (or assembly language, depending on the compiler), assembling converts single-line mnemonics to machine-level opcodes.
Note: An opcode is a special machine code that represents an instruction.
Also: An instruction is a processor-level unit of code that tells the processor what to do next.
16-Bit vs 32-Bit Code - What's The Difference?
When a computer first starts up, it is in 16-bit mode. It is the operating system that enters 32-bit mode and does everything else. But code written for 32-bit mode will most likely fail under 16-bit mode. That's because of references, such as "take this 32-bit value 00000100000001000000100010000001" , what would translate to "take this 16-bit value 0000010000000100" , while the other 8 bits will interfere with the next instruction, causing trouble.
Data vs BSS - Why Use BSS If We Have Data?
We can modify either section using code. But we can predefine one section, while we can't predefine the other.
With the data section, we use variables with predefined values, that we can change later on. But with the BSS section, we also use variables at predefined offsets (addresses), but we can't predefine the values, which usually start out with 0.
Using the BSS section helps reduce the size of our program file, somewhat.
There is another medium of storage, besides data and bss, about which I'll write in the next tutorial.
NASM Syntax - Some Things To Know
- Square brackets mean "at memory address."
- Operand-size prefixes (BYTE, WORD, DWORD, QWORD) are sometimes required and sometimes optional.
- <label_or_variable_name> means "the address of."
- Therefore, [<label_or_variable_name>] would mean "at memory address of."
- No more than one (1) memory reference allowed per instruction.
- Dollar sign ($) means the address of the current instruction.
- %define defines a single-line macro (similar to #define in C/C++) (see also, %undef).
- TIMES <n> <action> <n> times, do <action> (see also, %rep).
- You use 'extern' to tell NASM about external symbols that are not defined yet
and exist at the linking stage.
For more information about NASM and its syntax, you can refer to the NASM manual (NASM - The Netwide Assembler).
Byte, Word, Double-Word, Quad-Word - What Do Those Mean?
Those are just size specifications.
- A byte is 8 bits in size.
- A word is 16 bits in size (2 bytes).
- A double-word is 32 bits in size (4 bytes).
- A quad-word is 64 bits in size (8 bytes).
Data And BSS Sections - Defining Variables
You use DQ to define a quad-word, DD to define a double-word, DW to define a word, and DB to define a byte.
myvar dd 65 mystr db "Hello World!", 13, 10, 0The above code will define myvar as a double-word, initialized to 65, and mystr as an array of bytes (an ANSI string), initialized to "Hello World!\r\n\0" .
These things are usually used in the data section.
You use RESQ to reserve a number of quad-words, RESD to reserve a number of double-words, RESW to reserve a number of words, and RESB to reserve a number of bytes.
my_int resd 1 my_str resb 512The above code will reserve 1 double-word for my_int, and 512 bytes for my_str.
These things are usually used in the bss section.
Normally the things that we want to initially assign values to (ie "Hello World!", 65, etc.) are defined in the data section, while things that we don't know the value of, yet, we define in the BSS section.
More About Defining Data
We can either define data using quotes (ie "Hello World! ") or we can define data using ASCII numbers (ie 13, 10, 0). Let's take a look at how NASM assembles our data definitions:
Here's the output EXE code:
As you can see, our definitions sort of start from line 3 of the EXE file. They actually start from the end of line 2, but that's not in the picture.
On line 2 of the .asm file, we use 13 then 10, which makes a new line (hence, it's now line 3 of the PE file). Then we have 65 and "A" ; both of those evaluate to 'A' , because 65 is the ASCII code for the capital letter A. Then we also defined some things right after defining some_str.
So the data section is part of the executable file, and you can actually find an executable's data section, by examining its contents. But be careful about changing any part of an executable file, or you might cause the file to not be executable any more. I don't recommend changing any part of a PE file, unless you really know what you're doing.
Operand-Size Prefixes - When Are They Necessary?
Operand-size prefixes are required when operand sizes can't be infered (ie the PUSH instruction).
But operand-size prefixes can be used in other situations, too, although not required.
This is the first tutorial.
Edited by RhetoricalRuvim, 20 August 2011 - 07:23 PM.