[Day 21 of the C Language Inspector Training Camp] Zero-Basic Introduction to Assembly Language

foreword

Assembly language is a powerful programming language, and it is also a language that uses all the hardware features of a computer and can directly control the hardware. After learning well, you can do single-chip microcomputers, operating systems, and compilers. Anyway, low-level development is definitely needed.

Assembly language (assembly language) is a low-level language used in electronic computers, microprocessors, microcontrollers or other programmable devices, also known as symbolic language. In assembly language, mnemonics are used instead of opcodes for machine instructions, and address symbols or labels are used instead of addresses of instructions or operands.

In different devices, the assembly language corresponds to different machine language instruction sets, which are converted into machine instructions through the assembly process . There is a one-to-one correspondence between a specific assembly language and a specific machine language instruction set, and they cannot be directly transplanted between different platforms.

From the above, we can see the power of assembly language. Erqiang Ren Erqiang, what we need to do is to be able to understand the assembly language code and understand the basic knowledge in assembly language. First, let’s look at a real 408 professional course question. Out of the core part of the C language code and assembly code, we need to be able to see what this is for, and then solve the problem!
insert image description here

1. C language source file to assembly

Before converting and compiling C language source files, you need to configure the environment variables first, and configure the bin directory in the mingw64 we installed before to the path variable, so that you can use the gcc command directly in the black window! There are a bunch of configuration environment variable tutorials on the Internet, so I won’t be too long-winded. Let me talk about how to generate an assembly file and compile it with C language code to generate an executable file (this process is within the syllabus, but the probability of the exam is relatively low).

Compilation process (just understand, it is the scope of the outline, the probability of the test is not high)

  • The first step: main.c–>compiler–”main.s file (.s file is an assembly file, and the file contains assembly code)
  • Step 2: Our main.s assembly file - "assembler -" main.obj
  • Step 3: main.obj file – “Linker –” executable file exe

Next, let's use Clion to generate assembly code!
First enter the directory where the source file is located (here refers to the mytest directory):
insert image description here
Execute the following command to generate the assembly file (this method is different from the intel assembly code!):

gcc -S -fverbose-asm main.c

The following is the assembly code intel 32 that generates the same format as the postgraduate entrance examination:

gcc -m32 -masm=intel -S -fverbose-asm main.c

insert image description here

2. Assembly instruction format

Before looking at the assembly instructions, let's see how the CPU executes our program. As shown in the figure below, our compiled executable program, that is, main.exe, is placed in the code segment , and the PC pointer register stores A pointer always points to the instruction to be executed . After reading a certain instruction in the code segment, it will be handed over to the decoder for analysis . At this time, the decoder will know what to do. The computing unit adder in the CPU If you cannot directly add 1 to a variable a on the stack, you need to first load the stack, that is, the data on the memory, into the register, then use the adder to add 1, and then move it from the register to memory up.
insert image description here
A machine instruction is divided into the following two parts:
opcode field : characterizes the operating characteristics and functions of the instruction (unique identification of the instruction): different instruction opcodes cannot be the same.
Address code field : specifies the address code of the operand participating in the operation.
指令中指定操作数存储位置的字段称为地址码,地址码中可以包含存储器地址。也可包含寄存器编号。指令中可以有一个、两个或者三个操作数,也可没有操作数, 根据一条指令有几个操作数地址,可将指令分为零地址指令。一地址指令、二地址指令、三地址指令。4个地址码的指令很少被使用(考研考不到,这里不列了)
insert image description here
insert image description here
In the two-address instruction format, there are three types in terms of the physical location of the operand

  • Register-register (RR) type instructions: Multiple general-purpose registers or individual special-purpose registers are required, operands are fetched from registers, and the result of the operation is placed in another register . The machine executes register-register-type instructions very quickly and does not require memory access .
  • Register-memory (RS) type instructions: When executing such instructions, both memory units and registers must be accessed.
  • Memory-storage (SS) type instructions: the operation involves the memory unit, the numbers involved in the operation are placed in the memory, the operand is fetched from a certain unit of the memory, and the operation result is stored in another unit of the memory, so the machine executes Instructions require multiple accesses to memory.

Register English: register
memory English: storage

name feature common system Abbreviation English name
complex instruction set lengthen x86 CISC Complex lnstruction Set Computer
RISC Isometric arm RISC Reduced lnstruction Set Computin

3. Compile common instructions

3.1 Related Registers

insert image description here

3.2 Common commands

insert image description here

3.3 Data Transfer Instructions

insert image description here
insert image description here

3.4 Arithmetic/Logical Operation Instructions

insert image description here
insert image description here

3.5 Control Flow Instructions

insert image description here
insert image description here

3.6 Condition codes

The compiler implements the selection structure statement in the program through the condition code (flag bit) setting instruction and various transfer instructions.
Condition codes (flags)
In addition to the integer registers, the CPU maintains a set of condition code (flags) registers that describe the properties of the most recent arithmetic or logical operation. These registers can be tested to execute conditional branch instructions, the most commonly used condition codes are:

  • CF : carry (borrow) bit flag. Carry (borrow) bits after the latest unsigned integer addition (subtraction) operation. If there is a carry (borrow) bit, CF=1; otherwise, CF=0. Such as (unsigned) t <(unsigned) a, because the judgment size is subtraction.
  • ZF : Zero flag. Whether the calculation settlement of the most recent operation is 0. If the result is 0, ZF=1; otherwise ZF=0. As in (t==O).
  • SF : Symbol flag. The sign of the result of the nearest signed arithmetic operation. When negative, SF=1;
    otherwise SF=O.
  • OF : overflow flag. Whether the result of the latest signed number operation overflows, if overflow, OF=1; otherwise OF=0.

可见,OF和SF对无符号数运算来说没有意义,而CF对带符号数运算来说没有意义。
insert image description here
Common arithmetic logic operation instructions (add, sub, imul, or, and, shl, inc, dec, not, sal, etc.) will set condition codes. But there are two types of instructions that only set the condition code without changing any other registers, namely the cmp and test instructions, the cmp instruction behaves the same as the sub instruction, and the test instruction behaves the same as the and instruction, but they only set the condition code and do not update destination register.

Note: After the multiplication overflows, you can jump to the "overflow trap instruction". For example, int 0x2e is a trap instruction. What are the trap instructions.

4. How to define variables in assembly

We analyze the corresponding assembly for the assignment of integers, integer arrays, and integer pointer variables (floating point and character equivalent), first we write the following C code:

#include <stdio.h>
int main() {
    
    
    int arr[3] = {
    
    1, 2, 3};
    int *p;
    int i = 5;
    int j = 10;
    i = arr[2];
    p = arr;
    printf("i=%d\n", i);
    return 0;
}

Convert it into assembly language code (the postgraduate entrance examination for this type of topic is based on intel, if we are a windows system, then we can use the following command to generate):

gcc -m32 -masm=intel -S -fverbose-asm main.c
  • Next, let's analyze the converted assembly code. First, the **# number represents a comment**, and we can look at it from the position of the main label. When our C code is letting the CPU run, its 实所有的变量名都已经消失了,实际是数据从一个空间,拿到另一个空间的过程.
  • The variable in the stack is first defined at a low address or a high address, depending on the combination of the operating system and the CPU, yours may be different from mine, so there is no need to study it, it is meaningless (and it does not belong to the scope of the postgraduate entrance examination syllabus).
  • We access the space of all variables through the offset of the stack pointer (esp always stores the stack pointer, which can also be called the top pointer of the stack), to obtain the data corresponding to the variable memory space).
	.file	"main.c"
	.intel_syntax noprefix
	.text
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "i=%d\12\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	push	ebp	 #
	mov	ebp, esp	 #,
	and	esp, -16	 #,
	sub	esp, 48	 #,
 # main.c:2: int main() {
      
      
	call	___main	 # #调用main函数
 # main.c:3:     int arr[3] = {
      
      1, 2, 3};
	mov	DWORD PTR [esp+24], 1	 # arr,# 把常量1放入栈指针(esp寄存器存的栈指针)偏移量24个字节
	mov	DWORD PTR [esp+28], 2	 # arr,
	mov	DWORD PTR [esp+32], 3	 # arr,
 # main.c:5:     int i = 5;
	mov	DWORD PTR [esp+44], 5	 # i,
 # main.c:6:     int j = 10;
	mov	DWORD PTR [esp+40], 10	 # j,# 把常量40放入栈指针(esp寄存器存的栈指针)偏移44个字节,这个位置属于变量j。
 # main.c:7:     i = arr[2];
	mov	eax, DWORD PTR [esp+32]	 # tmp89, arr #把后面地址指向的的数据拿到eax寄存器内。
	mov	DWORD PTR [esp+44], eax	 # i, tmp89
 # main.c:8:     p = arr;
	lea	eax, [esp+24]	 # tmp90,# 把后面的地址拿到eax寄存器内。
	mov	DWORD PTR [esp+36], eax	 # p, tmp90
 # main.c:9:     printf("i=%d\n", i);
	mov	eax, DWORD PTR [esp+44]	 # tmp91, i
	mov	DWORD PTR [esp+4], eax	 #, tmp91
	mov	DWORD PTR [esp], OFFSET FLAT:LC0	 #, #把LC0的地址放到寄存器栈指针指向的位置!
	call	_printf	 #
 # main.c:10:     return 0;
	mov	eax, 0	 # _10,
 # main.c:11: }
	leave	
	ret	
	.ident	"GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
	.def	_printf;	.scl	2;	.type	32;	.endef

The offset value of the assembly that you transfer may be different from mine. This is okay. You just need to understand the assembly instructions and principles of variable assignment. The main instructions are mov, lea, and PTR. The following is the introduction of ptr

  • ptr-pointer (both pointer) abbreviation.
    In assembly, ptr is a prescribed word (both a reserved word) , which is used to temporarily specify the type. (It can be understood that ptr is a temporary type conversion, which is equivalent to the forced type conversion in C language)
    such as mov ax, bx; is to assign the value "in" the BX register to AX. Since both are registers, the length is fixed (word type), so there is no need to add "WORD"
    mov ax, word ptr [bx] ; it is to assign the data stored in the place where the memory address is equal to "the value of the BX register" to ax. Since only a memory address is given, I don’t know whether it is a byte or a word that I want to assign to ax, so I can use word to clearly indicate it; if not used, both (mov ax, [bx];) will pass a word by default in 8086, Both two bytes are given to ax.

Keywords in intel:

  • dword ptr long word (4 bytes)
  • word ptr is double byte
  • byte ptr is a byte

5. Select the actual combat of loop assembly

Write a piece of source code first!

#include <stdio.h>
int main()
{
    
    
    int i=5;
    int j=10;
    if (i< j)
    {
    
    
        printf("i is small\n");
    }
    for(i=0;i<5;i++)
        printf( "this is loop\n");
    return 0;
}

Generate assembly code:

	.file	"main.c"
	.intel_syntax noprefix


	.text #这里是文字常量区,存放了我们的字符串常量!LC0 LC1是我们要打印字符串的起始地址!
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "i is small\0"
LC1:
	.ascii "this is loop\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	push	ebp	 #
	mov	ebp, esp	 #,
	and	esp, -16	 #,
	sub	esp, 32	 #,
 # main.c:3: {
      
      
	call	___main	 #
 # main.c:4:     int i=5;
	mov	DWORD PTR [esp+28], 5	 # i,
 # main.c:5:     int j=10;
	mov	DWORD PTR [esp+24], 10	 # j,
 # main.c:6:     if (i< j)
	mov	eax, DWORD PTR [esp+28]	 # tmp89, i
	cmp	eax, DWORD PTR [esp+24]	 # tmp89, j #前者减去后者,然后设置条件码
	jge	L2	 #,  #判断条件码L2 如果符合jge就跳转到L2标签,否则往下执行,jge是根据条件码ZF和SF来判断的。
 # main.c:8:         printf("i is small\n");
	mov	DWORD PTR [esp], OFFSET FLAT:LC0	 #,
	call	_puts	 #
L2:
 # main.c:10:     for(i=0;i<5;i++)
	mov	DWORD PTR [esp+28], 0	 # i,
 # main.c:10:     for(i=0;i<5;i++)
	jmp	L3	 # # 无条件跳转到L3
L4:
 # main.c:11:         printf( "this is loop\n");
	mov	DWORD PTR [esp], OFFSET FLAT:LC1	 #,
	call	_puts	 #
 # main.c:10:     for(i=0;i<5;i++)
	add	DWORD PTR [esp+28], 1	 # i,
L3:
 # main.c:10:     for(i=0;i<5;i++)
	cmp	DWORD PTR [esp+28], 4	 # i,比较变量i的值与4的大小,并设置条件码,如果小于等于则直接跳转到L4
	jle	L4	 #,
 # main.c:12:     return 0;
	mov	eax, 0	 # _11,
 # main.c:13: }
	leave	
	ret	
	.ident	"GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
	.def	_puts;	.scl	2;	.type	32;	.endef

In this part, everyone understands the choice, the assembly instructions and principles of the cycle are sufficient, and the main instructions are cmp, ige, jmp, jle, etc. And understand that string constants exist in the literal constant area.

6. Function call assembly actual combat

Analysis of assembly principle of function call

The first thing to be clear is that the function stack grows downward. The so-called downward growth refers to the path extending from the high memory address to the low address. Therefore, the stack has a bottom and a top, and the address of the top of the stack is lower than that of the bottom of the stack.
For the CPU of the x86 system, the register ebp can be called the frame pointer or the base pointer (base pointer), and the register esp can be called the stack pointer (stack pointer). The points to be explained here are as follows
.
(1) ebp always points to the beginning of the stack frame (that is, the bottom of the stack) before it changes, so the purpose of ebp is to address in the stack (the role of addressing will be described in detail below).
(2) esp will move as the data is pushed into and out of the stack, that is, esp always points to the top of the stack.

insert image description here
As shown in Figure 2, assuming that function A calls function B, function A is called the caller, and function B is called the callee, then the function calling process can be described as follows: (1) First, the base of the stack of the caller (A
) The address (ebp) is pushed onto the stack to save the information of the previous task.
(2) Then assign the value of the top pointer (esp) of the caller (A) to ebp as the new base address (that is, the bottom of the stack of the callee B). The top of the stack of the original function is the bottom of the stack of the new function.
(3) Then open up (generally use the sub instruction) the corresponding space on this base address (the bottom of the stack of the callee B) as the stack space of the callee B.
(4)) After function B returns, the ebp of the current stack frame is restored to the top of the stack (esp) of caller A, so that the top of the stack restores the position before function B is called; then caller A pops from the restored top of the stack The ebp value (because this value is pushed onto the stack one step before the function call).
In this way, both ebp and esp restore the position before calling function B, that is, the stack restores the state before function B is called. Equivalent to (what the ret instruction does)

insert image description here
It can be understood as the following process!
insert image description here

Let’s first write a piece of code related to functions in C language:

#include <stdio.h>
int add(int a,int b){
    
    
    int ret;
    ret = a + b;
    return ret;
}
int main() {
    
    
    int a, b,ret;
    int*p;
    a= 5;
    p = &a;
    b = *p + 2;
    ret = add(a, b);
    printf("add result=%d\n",ret);
    return 0;
}

Generate assembly language code:

	.file	"main.c"
	.intel_syntax noprefix

	.text
	.globl	_add
	.def	_add;	.scl	2;	.type	32;	.endef
# add函数的入口
_add:
#把原有函数的栈基指针压栈
	push	ebp	 #
#修改栈基指针的指向(原栈顶将作为被调函数的栈基)
	mov	ebp, esp	 #,
# 栈顶向下移16个单位
	sub	esp, 16	 #,
 # main.c:4:     ret = a + b;
	mov	edx, DWORD PTR [ebp+8]	 # tmp93, a
	mov	eax, DWORD PTR [ebp+12]	 # tmp94, b
	add	eax, edx	 # tmp92, tmp93
	mov	DWORD PTR [ebp-4], eax	 # ret, tmp92
 # main.c:5:     return ret;
	mov	eax, DWORD PTR [ebp-4]	 # _4, ret
 # main.c:6: }
	leave	
	ret	 #函数返回,弹出压栈的指令返回地址回到main函数执行!
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "add result=%d\12\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
# 当调用main函数时按照下面大小初始化栈空间
_main:
	push	ebp	 #
	mov	ebp, esp	 #,
	and	esp, -16	 #,
	sub	esp, 32	 #,
 # main.c:7: int main() {
      
      
	call	___main	 #
 # main.c:10:     a= 5;
	mov	DWORD PTR [esp+16], 5	 # a,
 # main.c:11:     p = &a;
 #把a变量的地址拿到eax寄存器内
	lea	eax, [esp+16]	 # tmp91,
# 把eax寄存器内存放的值放进地址为esp+28的空间内。
	mov	DWORD PTR [esp+28], eax	 # p, tmp91
 # main.c:12:     b = *p + 2;
 # 先找到指针变量p指向的地址,将其放进寄存器
	mov	eax, DWORD PTR [esp+28]	 # tmp92, p
 # 将寄存器指向地址里面的内容拿到寄存器内。
	mov	eax, DWORD PTR [eax]	 # _1, *p_5
 # main.c:12:     b = *p + 2;

 # eax寄存器内的值增加2
	add	eax, 2	 # tmp93,
#将计算结果放进[esp+24]空间内。
	mov	DWORD PTR [esp+24], eax	 # b, tmp93

# 下面是函数调用实参传递的经典动作,从而理解值传递是如何实现的!
 # main.c:13:     ret = add(a, b);
	mov	eax, DWORD PTR [esp+16]	 # a.0_2, a
	mov	edx, DWORD PTR [esp+24]	 # tmp94, b
	mov	DWORD PTR [esp+4], edx	 #, tmp94
	mov	DWORD PTR [esp], eax	 #, a.0_2
	call	_add	 #
# 将计算结果赋值进ret变量内。
	mov	DWORD PTR [esp+20], eax	 # ret, tmp95
 # main.c:14:     printf("add result=%d\n",ret);
	mov	eax, DWORD PTR [esp+20]	 # tmp96, ret
	mov	DWORD PTR [esp+4], eax	 #, tmp96
	mov	DWORD PTR [esp], OFFSET FLAT:LC0	 #,
	call	_printf	 #
 # main.c:15:     return 0;
	mov	eax, 0	 # _10,
 # main.c:16: }
	leave	
	ret	
	.ident	"GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
	.def	_printf;	.scl	2;	.type	32;	.endef

The figure below is the schematic diagram of the main function calling the add function!
insert image description here
In this part, everyone understands the principle of indirect access to pointer variables and the principle of function calling. Up to now, everyone has been very clear about the operation of each part of the C language on the machine. The main instructions in this part are add, sub, call, ret, etc.

7. C language source file to machine instruction

Next, we also need to grasp the offset value of the machine code when the function is called. What we have transferred before is only assembly, without machine code. How to get the machine code, you need to execute the following two instructions. The first one: gcc -m32 -g
-o main main.c (Mac is the same)
The second item: objdump --source main.exe >main.dump (Mac removes the .exe suffix and writes it as main)
. code! ! !

main.exe:     file format pei-i386


Disassembly of section .text:

00401000 <___mingw_invalidParameterHandler>:
  401000:	f3 c3                	repz ret 
  401002:	8d b4 26 00 00 00 00 	lea    0x0(%esi,%eiz,1),%esi
  401009:	8d bc 27 00 00 00 00 	lea    0x0(%edi,%eiz,1),%edi

00401010 <_pre_c_init>:
  401010:	83 ec 1c             	sub    $0x1c,%esp
  401013:	31 c0                	xor    %eax,%eax
  401015:	66 81 3d 00 00 40 00 	cmpw   $0x5a4d,0x400000
  40101c:	4d 5a 
  40101e:	c7 05 8c 53 40 00 01 	movl   $0x1,0x40538c
  401025:	00 00 00 
  401028:	c7 05 88 53 40 00 01 	movl   $0x1,0x405388
  40102f:	00 00 00 
  401032:	c7 05 84 53 40 00 01 	movl   $0x1,0x405384
  401039:	00 00 00 
  40103c:	c7 05 20 50 40 00 01 	movl   $0x1,0x405020
  401043:	00 00 00 
  401046:	74 49                	je     401091 <_pre_c_init+0x81>
  401048:	a3 08 50 40 00       	mov    %eax,0x405008
  40104d:	a1 98 53 40 00       	mov    0x405398,%eax
  401052:	85 c0                	test   %eax,%eax
  401054:	74 2d                	je     401083 <_pre_c_init+0x73>
  401056:	c7 04 24 02 00 00 00 	movl   $0x2,(%esp)
  40105d:	e8 4a 15 00 00       	call   4025ac <___set_app_type>
  401062:	e8 4d 15 00 00       	call   4025b4 <___p__fmode>
  401067:	8b 15 a8 53 40 00    	mov    0x4053a8,%edx
  40106d:	89 10                	mov    %edx,(%eax)
  40106f:	e8 dc 05 00 00       	call   401650 <__setargv>
  401074:	83 3d 1c 30 40 00 01 	cmpl   $0x1,0x40301c
  40107b:	74 63                	je     4010e0 <_pre_c_init+0xd0>
  40107d:	31 c0                	xor    %eax,%eax
  40107f:	83 c4 1c             	add    $0x1c,%esp
  401082:	c3                   	ret    
  401083:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
  40108a:	e8 1d 15 00 00       	call   4025ac <___set_app_type>
  40108f:	eb d1                	jmp    401062 <_pre_c_init+0x52>
  401091:	8b 15 3c 00 40 00    	mov    0x40003c,%edx
  401097:	81 ba 00 00 40 00 50 	cmpl   $0x4550,0x400000(%edx)

Guess you like

Origin blog.csdn.net/apple_51931783/article/details/129307950