Introduction to Assembly Language

Friends with a bit of computer knowledge must know that computers only recognize 0 and 1. Back in the day, to write a program, you had to use 0 and 1, haha, cool, right? So, the admiration for programmers might have originated from that time. Later, people found it too inconvenient to write programs using 0 and 1; it was not only hard to get started but also difficult to understand when looking back. For these reasons, assembly language was created.

Assembly language uses mnemonic codes to replace various combinations of 0 and 1, which represent various instructions. This makes it much easier to write and read to some extent (an old cow: it’s much easier). However, assembly is still not convenient; it is still cumbersome to write and maintain. As people gradually needed to write larger programs, high-level languages were invented, such as Basic, Pascal, C, C++, etc. The emergence of these languages greatly reduced the difficulty of program development (an old cow: it reduced it so much that I could write programs with my knees). Programs that took a long time to develop in assembly can now be completed quickly and easily. Especially in recent years, the widespread popularity of visual programming has diminished the mystique of programmers; terms like “Coder” are now everywhere. The worst off is assembly language, which has overnight become a low-level language, a vulgar language, the laborer who eats garlic and doesn’t brush his teeth, the scoundrel who fills up his car without paying, the Icelanders who spit on the bus, and so on (assembly language: sob… I’m done for).

However, assembly language still has its inherent advantages because it corresponds directly to the CPU’s internal instructions. Therefore, in some special cases, it must be implemented in assembly, such as accessing hardware ports or writing viruses. Moreover, the executable files generated are highly efficient and very small, making it easy to write small programs. Writing a key generator in assembly is also quite easy, as you don’t have to worry about how to revert it to a language you are familiar with. After all this, let’s get to the point (the audience faints): since computers only recognize 0 and 1, all files stored on computers are also stored in binary form, including executable files.

So, you just need to find a hex editor like Ultra Edit to directly open and view the executable file, haha, if you can understand it. You will find that what you see are all hexadecimal values (every 4 bits of binary can be converted to a hexadecimal digit). This is the specific content of the executable file, which, of course, includes the code of the executable file (an old cow: how nostalgic). Haha, at this point, do you feel like looking at these things is somewhat… what should I say?

These things look like an alien script, and no one can analyze them using this stuff. Hence, corresponding software has been developed to convert these hexadecimal values into corresponding assembly code, allowing us to analyze other people’s software. This is what is known as reverse engineering.

Haha, clever you must be thinking that if you find the part of the software that calculates the registration code and analyze it to understand its calculation method, then you wouldn’t have to register the software through monetary means, right? Of course, you can also revert this calculation process to any programming language you are familiar with. The compiled program is called a key generator, which functions to calculate the registration code for a specific software (haha, don’t you often see such statements in software? “It is prohibited to create and provide key generators and cracking programs for this software; reverse engineering, such as disassembly and decompilation, is prohibited”).

We can understand the author’s feelings in doing this, as they have put so much effort into their software. Therefore, I hope you do not learn cracking just because you can’t afford the registration fee.

In summary, the introduction above may be a bit idealistic. The analysis method mentioned above is known as static analysis, and commonly used tools for this type of analysis include W32DASM, IDA, and HIEW. Static analysis, as the name suggests, analyzes software by only viewing its disassembled code. Generally, if you just want to crack software, static analysis is sufficient. However, to truly understand the registration algorithm, dynamic analysis is often needed, which involves using a debugger to execute the program while analyzing it.

I have said so much just to tell you the importance of assembly. I do not require you to be proficient, but at least you should be able to understand it; otherwise, what can you talk about analysis? Although some people have managed to crack several software without knowing any assembly, isn’t that a bit tragic? Do you want to spend your whole life cracking software?

In fact, you really don’t need to fear assembly. It looks scary, but it is actually quite similar to memorizing the properties and methods of controls. You can handle so many MFCs, so how many assembly commands are there? Moreover, assembly is not only useful for cracking software; it is also useful in many other areas and has significant applications. Therefore, I believe mastering assembly is an obligation: you just need to believe it is not difficult.

Let me first tell you about the composition of the CPU: the CPU’s task is to execute the sequence of instructions stored in memory. For this purpose, in addition to performing arithmetic and logical operations, it also needs to undertake the data transmission tasks between the CPU, memory, and I/O. Early CPU chips included only two major parts: the arithmetic logic unit and the controller. In recent years, to better match the speed of memory with that of the arithmetic unit, a cache memory has been introduced into the chip (do you know why P4 is much more expensive than P4 Celeron?). (Suddenly! An object flew in: what are you talking about? We don’t want to design a CPU). Why are you in such a hurry? Because assembly is relatively “low-level” ;), it directly operates on hardware. You might think this is like using VB, where you can use variables whenever you want. If you don’t understand the internal workings of the CPU, how can you read assembly code? (Suddenly! Another voice: why don’t you hurry up and say something important?)

Besides the cache memory, the CPU can be roughly divided into three parts:

1. Arithmetic Logic Unit (ALU) used for performing arithmetic and logic operations. This part is not very relevant to us, and we don’t need to worry about it.

2. Control Logic. This is also not very relevant to us. 3. This is the most important part: Working Registers, which play a crucial role in the computer. Each register is equivalent to a storage unit in the arithmetic unit, but its access speed is incredibly fast, much faster than memory. It is used to store various information needed or obtained during calculations, including operand addresses, operands, and intermediate results of operations. Below, we will specifically introduce these registers.

Before introducing them, it is necessary to mention some basic knowledge. Do you know what 32-bit means? It means the register is 32 bits, which is confusing~~ equals saying nothing. In a CPU, a binary bit is considered one bit, eight bits make a byte, and in memory, information is stored in bytes. Each byte unit is assigned a unique storage address, called a physical address. When accessing the corresponding memory, it is done through this address. What can eight binary bits express? They can express all ASCII codes, meaning one memory unit can store one English character or number, while Chinese characters require Unicode representation, which means two memory units are needed to store one Chinese character. Sixteen bits equal two bytes; this is not difficult to understand. Of course, with sixteen bits, there are definitely thirty-two bits and sixty-four bits, where thirty-two bits are called double words and sixty-four bits are called quad words. The CPUs we use today are mostly 32-bit, unless you are using a 286 or earlier. Naturally, the registers in the CPU are also 32-bit, meaning one register can hold 32 zeros or ones (this does not include segment registers).

Generally speaking, you need to master sixteen registers, and I will introduce them one by one:

First, let’s introduce the general-purpose registers.

There are a total of eight: EAX, EBX, ECX, EDX, ESP, EBP, EDI, ESI. Among them, EAX-EDX can be called data registers. Besides direct access, you can also access their high and low sixteen bits (did I mention they are 32 bits?). Their low sixteen bits are obtained by removing the E, meaning the low sixteen bits of EAX are AX. Moreover, their low sixteen bits can also be accessed in eight bits, meaning AX can be further decomposed into AH (high eight bits) and AL (low eight bits). The other three registers can be inferred by yourself. This way, you can handle various situations: if you want to operate on an eight-bit data, you can use MOV AL (eight-bit data) or MOV AH (eight-bit data). If you want to operate on a sixteen-bit data, you can use MOV AX (sixteen-bit data). For thirty-two bits, you would use MOV EAX (thirty-two-bit data). You might still not understand what I said, that’s okay, take your time. Let me draw a rough diagram for you, although it doesn’t look very pretty: ─────────────────────── │ │ │ │ │ │ │ │ │ High Sixteen Bits EAX AH AX AL │ │ │ │ │ │ │ │ │ ─────────────────────── (Oh no… why can’t this diagram display correctly? I’ve redrawn it three times). Do you understand? If you don’t, that’s fine; just understand as much as you can.

These four registers are mainly used to temporarily store operands, results, or other information needed during calculations. The other four registers, ESP, EBP, EDI, and ESI, can only be accessed by words, and their main purpose is to provide offset addresses during memory addressing. Therefore, they can be called pointer or index registers. By the way, since the 386, all registers can be used to store memory addresses. (Here’s a little knowledge: have you ever seen a format like [EBX] when cracking? This means that EBX currently holds a memory address, and what you actually want to access is the value stored in that memory unit).

Among these registers, ESP is called the stack pointer register. The stack is a very important concept; it is a storage area that works in a “last in, first out” manner, and it must exist in the stack segment, so its segment address is stored in the SS register. It only has one entrance and exit, so there is only one stack pointer register. The content of ESP always points to the current top of the stack. You might still not understand what I’m saying, so let me give you an example. You know how laborers build houses, right? Let’s say there are two laborers: one laborer (hereafter referred to as laborer A) is laying bricks on the ground, while the other laborer (hereafter referred to as laborer B) hands bricks to laborer A. Laborer A is lying on the ground, and next to him are the bricks that laborer B brought from a distance. Laborer A picks them up and uses them, while laborer B puts more bricks on top of the pile as he brings them. This is how the stack works; it starts with a high address, and whenever data is pushed onto the stack, it stores it in the direction of lower addresses. The corresponding push instruction is PUSH. Whenever data is pushed onto the stack, ESP changes accordingly, and it always points to the last data pushed onto the stack. Later, if you want to use the data pushed onto the stack, you can use a pop instruction to take it out. The corresponding instruction is POP, and after executing the POP instruction, ESP increases by the corresponding data size.

Especially now that we are in the Win32 system, the role of the stack cannot be ignored. The data used by APIs is transmitted via the stack; that is, the data to be transmitted is first pushed onto the stack, and then a CALL is made to the API function. The API function will use the pop instruction to pop the corresponding data from the stack and then perform operations. You will understand the importance of this point later. Many software that compares plaintext generally push both the true and false registration codes onto the stack before the key CALL. Then, after the CALL, they compare them by popping from the stack. Therefore, as long as you find a key CALL, you can set a breakpoint at the push instruction to see the true registration code. The specific content will be detailed later; we will not discuss it in this chapter.

Additionally, there is EBP, which is called the base pointer register. It can be used together with the stack segment register SS to determine the address of a certain memory unit in the stack. ESP indicates the offset address of the segment, while EBP serves as a base address in the stack area for accessing information in the stack. ESI (source index register) and EDI (destination index register) are generally used in conjunction with the data segment register DS to determine the address of a certain memory unit in the data segment. These two index registers have automatic increment and decrement functions, making them convenient for indexing. In string processing instructions, ESI and EDI are used as implicit source and destination index registers, respectively, with ESI used with DS and EDI with the extra segment ES to achieve addressing in the data segment and extra segment, respectively. It’s okay if you don’t understand it for now.

Next, let’s introduce the special registers, haha, don’t be scared by this name; it sounds quite professional. The so-called special registers are two: EIP and FLAGS.

First, let’s talk about EIP. You could say that EIP is the most important register of all. It stands for the instruction pointer register, which stores the offset address in the code segment. During the execution of the program, it always points to the starting address of the next instruction. It works with the segment register CS to determine the physical address of the next instruction. When this address is sent to memory, the controller can fetch the next instruction to be executed. Once the controller obtains this instruction, it immediately modifies the content of EIP so that it always points to the starting address of the next instruction. It can be seen that the computer uses the EIP register to control the execution flow of the instruction sequence. Those jump instructions achieve their purpose by modifying the value of EIP.

Next, let’s talk about FLAGS, the flag register, also known as the program status word (PSW). This register stores condition flags, control flags, and system flags.

Actually, we don’t need to understand it too much; for now, you just need to know how it works. Let me give you an example:

Cmp EAX, EBX ; Subtract EAX from EBX JNZ 00470395 ; If not equal, jump here;

These two instructions are very simple; they compare the numbers stored in the EAX and EBX registers. After the Cmp instruction is executed, the corresponding value is set on the ZF (zero flag) in FLAGS. If the result is 0, meaning they are equal, ZF is set to 1; otherwise, it is set to 0. Other flags include OF (overflow flag), SF (sign flag), CF (carry flag), AF (auxiliary carry flag), PF (parity flag), etc.

You don’t need to understand all this right now; just using the corresponding transfer instructions will be enough.

Finally, let’s introduce the segment registers (who said cherry blossom? It wasn’t me). There are a total of six of these registers: CS (code segment), DS (data segment), ES (extra segment), SS (stack segment), and FS and GS (which are also extra segments). Actually, in the Win32 environment, segment registers are not as important as they were in the DOS era. So, we just need to know about them. After all this, I believe you have a general understanding of the CPU, right? What? You still don’t understand anything? Haha, don’t be discouraged; please believe it is my fault for not explaining clearly. You can refer to some books. I always think it is very necessary to have a book on assembly language on your desk. I recommend the Tsinghua edition of “80×86 Assembly Language Programming” edited by Shen Meiming, priced at 46 yuan. Next, let’s talk about some common assembly instructions (since there are already relevant posts, I will only pick out some of the most commonly used instructions that need to be mastered; for more content, please refer to the book).

CMP A, B Compare A and B, where A and B can be registers or memory addresses, but both cannot be memory addresses. This instruction is very common; many plaintext comparison software uses this instruction. MOV A, B Move the value of B to A, where A and B can be registers or memory addresses, but both cannot be memory addresses. XOR a, a Exclusive OR operation, mainly used to clear a. LEA Load effective address, for example, LEA DX, string loads the address of the character into the DX register. PUSH Push onto the stack. POP Pop from the stack. ADD Addition instruction Format: ADD DST, SRC Operation: (DST)<- (SRC) + (DST) SUB Subtraction instruction Format: SUB DST, SRC Operation: (DST)<- (DST) – (SRC) MUL Unsigned multiplication instruction Format: MUL SRC Operation: Byte operation (AX)<- (AL) * (SRC); Word operation (DX, AX)<- (AX) * (SRC); Double word operation: (EDX, EAX)<- (EAX) * (SRC) DIV Unsigned division instruction Format: DIV SRC Operation: Byte operation: 16-bit dividend in AX, 8-bit divisor as source operand, 8-bit quotient in AL, 8-bit remainder in AH. Represented as: (AL)<- (AX) / (SRC) quotient, (AH)<- (AX) / (SRC) remainder. Word operation: 32-bit dividend in DX, AX. DX is the high word, 16-bit divisor as source operand, 16-bit quotient in AX, 16-bit remainder in DX. Represented as: (AX)<- (DX, AX) / (SRC) quotient, (DX)<- (DX, AX) / (SRC) remainder. Double word operation: 64-bit dividend in EDX, EAX. EDX is the high double word; 32-bit divisor as source operand, 32-bit quotient in EAX, 32-bit remainder in EDX. Represented as: (EAX)<- (EDX, EAX) / (SRC) quotient, (EDX)<- (EDX, EAX) / (SRC) remainder. NOP No operation, which can be used to erase corresponding statements, so, hehe… CALL Call a subroutine; you can understand it as a procedure in high-level languages. Control transfer instructions: JE or JZ Jump if equal. JNE or JNZ Jump if not equal. JMP Unconditional jump. JB Jump if less than. JA Jump if greater than. JG Jump if greater than. JGE Jump if greater than or equal to. JL Jump if less than. JLE Jump if less than or equal to. In summary, the above instructions are the more common ones that need to be mastered, but there are definitely more instructions to learn; I hope you can find out more about them in your spare time by looking for relevant tutorials.

I forgot to mention that I will also post the number system conversion:

First, let’s talk about converting binary to decimal: The decimal number corresponding to a binary number is obtained by multiplying each binary digit by its corresponding weight and summing them up. For example: 10100 = 2^4 + 2^2, which equals decimal 20. 11000 = 2^4 + 2^3, which equals decimal 24.

Next, let’s talk about converting decimal numbers to binary: I’m not sure how many methods there are, but I will only talk about the simplest one – division: Continuously divide the integer part of the decimal number by 2, recording the remainders until the quotient is 0. Example: N = 34D (to explain, you might see a letter added to some numbers, which indicates the number system; decimal numbers use D, binary numbers use B, octal numbers use O, hexadecimal numbers use H) 34/2=17 (a0=0) 17/2=8 (a1=1) 8/2=4 (a2=0) 4/2=2 (a3=0) 2/2=1 (a4=0) 1/2=0 (a5=1) Therefore, N=34D=100010B.

For the decimal number’s fractional part, you should continuously multiply by 2 and record the integer parts until the fractional part of the result is 0.

Hexadecimal numbers and binary numbers, as well as decimal numbers, are relatively simple to convert. The base of hexadecimal numbers is 16, with 16 digits: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F. Among them, A represents decimal 10, and the rest follow suit. Their relationships with binary and decimal numbers are as follows: 0H=0D=0000B, 1H=1D=0001B, 2H=2D=0010B, 3H=3D=0011B, 4H=4D=0100B, 5H=5D=0101B, 6H=6D=0110B, 7H=7D=0111B, 8H=8D=1000B, 9H=9D=1001B, AH=10D=1010B, BH=11D=1011B, CH=12D=1100B, DH=13D=1101B, EH=14D=1110B, FH=15D=1111B. Therefore, to convert between binary and hexadecimal, you only need to group them into four bits from low to high and directly use hexadecimal to represent them: Example: 1000 1010 0011 0101 8 A 3 5. To convert hexadecimal to binary, you only need to represent each digit with four bits of binary: Example: A B 1 0 1010 1011 0001 0000. Finally, for the conversion between hexadecimal and decimal numbers: Hexadecimal to decimal: The decimal number corresponding to a hexadecimal number is obtained by summing the products of each hexadecimal digit and its corresponding weight. Example: N = BF3CH = 11 * 16^3 + 15 * 16^2 + 3 * 16^1 + 12 * 16^0 = 11 * 4096 + 15 * 256 + 3 * 16 + 12 * 1 = 48956D. Decimal to hexadecimal: I will only talk about the simplest division method: Continuously divide the integer part of the decimal number by 16, recording the remainders until the quotient is 0. Example: N = 48956D 48956/16=3059 (a0=12) 3059/16=191 (a1=3) 191/16=11 (a2=15) 11/16=0 (a3=11) Therefore, N=48956D=BF3CH.

Through the above introduction, I don’t know if you understood anything. If you did, please refer to books and carefully review the parts I didn’t cover and the ones I did. If you didn’t understand anything, then you need to read books more than ever; don’t lose confidence in learning. If you carefully read the introduction to the CPU and understand the concept of registers, and then learn the assembly instructions, you can get started. If you study and memorize carefully, you will find that it is not as difficult as you imagined. In a week, you can grasp the basics, but just grasping is not enough; at least you should be able to read assembly code. If you want to learn it well, you should also read the later sections and write some small programs to practice. Of course, mastering assembly is not something that can be done in a day, a month, or two months, but as long as you have perseverance, what can’t you handle? The CPU was made by humans, and instructions are just a part of it; if they can build a CPU, why can’t you learn to use it? Post-Class FAQ

Q: I learned 8086/8088 before and wrote programs under DOS; can I do it? A: Absolutely, compared to 8086/8088, the current CPU has not added many new instructions in basic instruction aspects. You just need to understand the changes in each register and supplement your knowledge of Windows programming. Moreover, since you have written programs in assembly under DOS, you must be quite familiar with debugging tools like Debug, so you have an inherent advantage.

Q: Assembly is not a problem for me, but why can’t I get started? A: Haha, there are quite a few old birds like this; they use assembly quite proficiently, but just because of experience, they feel stuck. Many people were like this back then, at least I was; when I saw CALL, I just followed it, haha, I ended up following quite a few APIs. So, for these experts, you just need to practice more and master some analysis techniques.

Q: I have never learned programming; can I learn assembly? A: Generally speaking, yes. However, I hope that learning assembly will not make you lose confidence in learning other high-level languages. 🙂 Answering the netizens’ questions Q: Can registers be used freely? Are there any restrictions? Can variables be placed in any register when writing a program?

A: Haha, I will now answer the question from the friend above. Registers have their usage mechanisms, and each register has a specific function. For example, data registers (EAX-EDX) are general-purpose registers, meaning any data can be stored in them. However, they can also be used for their specific purposes. For example: EAX can be used as an accumulator, so it is the main register for arithmetic operations. In multiplication and division instructions, it is specified to store operands. For example, in multiplication, you can use AL or AX or EAX to hold the multiplicand, while AX or DX:AX or EAX or EDX:EAX is used to hold the final product.

EBX is generally used as a base register when calculating memory addresses. ECX is often used to store count values, such as in shift instructions where it holds the shift amount, as an implicit counter in loops and string processing instructions.

Finally, there’s EDX, which is generally used to store a double word by combining DX and AX to hold a double word number (do you remember what a double word is? For example, if you have a binary number 01101000110101000100100111010001, you can store it by placing 0110100011010100 (the high sixteen bits) in DX and 0100100111010001 (the low sixteen bits) in AX. This number is represented as DX:AX). Of course, you can also use EDX alone to store this number. Therefore, you can also use EDX:EAX to store a 64-bit number; you can infer this.

As for ESP, EBP, EDI, and ESI, I have already introduced them roughly, so I won’t repeat them here.

Of course, there are other restrictions, as we are only looking at the assembly code of the program (the written code should not have errors, right?), and we do not need to master them. If you are interested, you can look at related books.

Also, regarding your last question, “Can variables be placed in any register when writing a program?” I don’t understand what you mean by that. I think you might be confused about some points; the term variable usually appears in high-level languages, and when you write programs in high-level languages, you don’t need to understand those registers; they have nothing to do with high-level languages. However, ultimately, high-level languages still convert the programs you write into operations on registers and internal memory.

<End of this chapter>

Leave a Comment