Assembly Language

Posted 2017-10-14 20:13 +0800 by ZhangJie ‐ 8 min read

分享:  

处理器是算逻运算、控制操作的执行部件,它只能识别机器指令并执行动作。机器指令是一系列的0、1字符串,本质上对应了总线上的高低电平信号,所以机器语言都是特定于硬件的。

由于0、1字符串很难记忆,用机器语言开发是一个老大难的问题,汇编语言因此被开发出来用于代替机器语言。汇编指令只是机器指令中操作码的助记符,因此汇编语言仍然是机器强相关的,不同的处理器其对应的汇编指令也不同。

学习汇编语言有助于理解:

  • 程序是如何与操作系统、处理器、bios进行交互的;
  • 数据如何在内存中以及外设中表示的;
  • 处理器如何访问、执行指令;
  • 指令如何访问、处理数据;
  • 程序如何访问外设;

其他使用汇编语言的优势:

  • 消耗更少的内存和处理器执行时间;
  • 允许以更简单的方式来完成硬件特定的复杂作业;
  • 适用于时间敏感的作业;
  • 适用于编写中断服务程序和内存驻留程序;

1.1 PC硬件的基本特征

机器指令是0、1字符串,分别表示ON、OFF,对应数字信号的高低电平。机器中的最低存储单位是bit,通常8bit构成一个byte,为了对数据传输过程中传输数据的有效性进行检查,通常会在数据byte发送之后再追加一个奇偶校验bit。

  • 奇校验:保证8bit数据+1bit校验位中的1的个数为奇数;
  • 偶校验:保证8bit数据+1bit校验位中的1的个数为偶数;

发送方、接收方遵循相同的奇偶校验规则,如果接收方收到数据后发现奇偶校验不正确,则表示可能硬件出错,或者出现了电平扰动。

处理器支持如下数据尺寸:

|:—|:——| |Word|2 bytes| |Doubleword|4 bytes| |Quadword|8 bytes| |Paragraph|16 bytes| |Kilobyte|2^10 bytes| |Megabyte|2^20 bytes|

二进制 & 十六进制系统:

二进制天然适用于计算机计算领域,0、1刚好代表数字电路中的高低电平;而十六进制是用于对比较长的二进制数值进行更加优雅地简写,使我们表示起来更加清晰、简单。

二进制、十六进制的相关运算,特别是涉及到原码、反码、补码、移码的运算,需要重点了解下,建议参考《计算机组成原理》相关章节。

访问内存中的数据:

处理器控制指令执行的过程可以简化为”取指令-指令移码-指令执行“的循环体,一个”取指令-指令译码-指令执行“周期称之为一个机器周期。

  • 取指周期:根据CS、IP从内存指定位置取指令,并存储到指令寄存器IR;
  • 译码周期:根据IR中的指令,分析出操作码OP、操作数或操作数地址;
  • 执行周期:根据分析出的OP、操作数或操作地址信息执行相应的动作;

Intel架构的处理器在内存中存储时是采用的小端字节序,意味着一个多字节数值的低字节部分将在低地址存储,高字节部分将在高地址存储,但是在处理器寄存器中存储时低字节部分就在低字节,高字节部分就在高字节,所以在处理器寄存器、内存之间存储、加载数据时需要做字节序方面的转换。

以处理器寄存器中数值0x1234为例,现在要将其存储到内存中,处理器先将0x34存储到内存低地址,然后再见0x12存储到内存高地址;假如内存中有数据0xabcd,现在要将其加载到处理器寄存器中,加载时也会做对应的处理,将0xab放在寄存器高位,将0xcd放在寄存器低位。

指令中的操作数地址,又有多种不同的寻址方式,立即数寻址、直接寻址、间接寻址、寄存器寻址等,这里后面会做相应的介绍。

1.2 开发环境配置

汇编指令特定于处理器的,因此不同的处理器系列、型号对应的汇编指令可能也会有差异,这里使用的是Intel-32架构的处理器,使用汇编器NASM进行汇编操作,其他可选的汇编器还有MASM、TASM、GAS等。

1.3 基本语法

汇编程序通常包括3个节,分别是data、bss、text节:

  • data,用于声明初始化的变量和常量;
  • bss,用于声明未初始化的变量,这部分不会出现在编译后的程序中;
  • text,用于保存程序指令;

text节中必须包括"global ${entry}“声明,${entry}是程序入口,通常定义未_start,见文生义嘛。

汇编程序中的注释均以”;“开头,直到所在行结束。

汇编语言程序包括3种不同类型的语句:

  • 可执行汇编指令;
  • 传递给汇编器的指令或伪操作;
  • 宏;

汇编语言语句遵循如下结构:

[标识] 助记符 [操作数] [;注释]

[]内部的部分是可选的,尤其是标识和注释部分,根据汇编指令的不同,有无操作数、操作数个数、操作数类型等均有所不同。汇编指令中包括了操作码和操作数相关信息,这里的助记符其实就是操作码的符号表示。

下面是应用了上述基本语法的示例程序:

hello.asm

section	.text
   global _start     ;must be declared for linker (ld)
	
_start:	            ;tells linker entry point
   mov	edx,len     ;message length
   mov	ecx,msg     ;message to write
   mov	ebx,1       ;file descriptor (stdout)
   mov	eax,4       ;system call number (sys_write)
   int	0x80        ;call kernel
	
   mov	eax,1       ;system call number (sys_exit)
   int	0x80        ;call kernel

section	.data
msg db 'Hello, world!', 0xa  ;string to be printed
len equ $ - msg     ;length of the string

在Linux平台下由汇编文件构建可执行程序包括如下两个步骤:

  • 汇编,nasm -f elf -o hello.o hello.asm
  • 连接,ld -m elf_i386 -e _start -o hello hello.o

构建完成,即可在命令行执行./hello来进行测试。

1.4 内存分段

前面一节介绍了汇编语言中的section(节),这些节也代表着各种各样的内存segment(段)。将前面示例代码hello.asm中的section关键字用segment代替,依然可以用相同的方法构建成功并得到相同的测试结果。

汇编语言中常见的segment(段)包括:

  • data segment,数据段代表了data section和bss section。其中data section用于存储初始化后的全局变量和全局变量;bss section用于声明未初始化的全局变量和静态变量,在程序运行时会被初始化未0值。
  • code segment,代码段也就是text节,用于存储程序指令;
  • stack segment,堆栈段用于分配临时变量、调用函数时传递参数信息等;

1.5 寄存器

处理器主要是用来进行计算,计算所需要的数据来自于内存,但是处理器存取内存数据需要的时间比较长,为了ALU加速存取操作数,处理器里面内置了寄存器,将内存中的数据先加载到寄存器中,然后ALU对寄存器中的数据进行计算,最后再将计算结果搬回内存。

处理器中的寄存器主要包括如下几类:

  • 通用目的寄存器,包括数据寄存器(EAX、EBX、ECX、EDX)、指针寄存器(EIP、ESP、EBP)、索引寄存器(ESI、EDI);

函数调用过程中会形成栈帧,ebp寄存器指向的是栈帧的栈底,esp指向的值栈帧的栈顶。通过ebp便于定位传递给函数的参数、返回地址信息,栈帧开始构建的时候,首先就会将caller的ebp压栈,然后将当前栈帧栈顶esp赋值给ebp作为新的栈帧的栈底,后面esp减去一个值N,[esp-N,ebp)就是新的栈空间。

  • 段寄存器,包括ECS、ESS、EDS;

8086里面,CS包括代码段的起始地址,从80386进入保护模式开始,CS里面变成了段选择符,具体的代码段起始地址要到gdb里面去查。DS存储数据段的起始地址,SS存储堆栈段的起始地址。

  • 控制寄存器,32位flags寄存器和32位指令指针寄存器共同称为控制寄存器。

常见的flag标志位包括:OF(溢出)、DF(字符串比较方向)、IF(是否允许中断)、TF(是否单步执行)、SF(符号位)、ZF(比较结果)、AF(辅助进位)、PF(1数量是否为奇数)、CF(是否进位)。

下面是一个使用多个寄存器的示例程序:

section	.text
   global _start	 ;must be declared for linker (gcc)
	
_start:	         ;tell linker entry point
   mov	edx,len  ;message length
   mov	ecx,msg  ;message to write
   mov	ebx,1    ;file descriptor (stdout)
   mov	eax,4    ;system call number (sys_write)
   int	0x80     ;call kernel
	
   mov	edx,9    ;message length
   mov	ecx,s2   ;message to write
   mov	ebx,1    ;file descriptor (stdout)
   mov	eax,4    ;system call number (sys_write)
   int	0x80     ;call kernel
	
   mov	eax,1    ;system call number (sys_exit)
   int	0x80     ;call kernel
	
section	.data
msg db 'Displaying 9 stars',0xa ;a message
len equ $ - msg  ;length of message
s2 times 9 db '*'

1.6 系统调用

系统调用是操作系统内核提供的用户态、内核态之间的接口,用户态通过系统调用访问内核服务。

如何通过在汇编程序里面使用系统调用呢?

  • 在EAX寄存器里面设置系统调用号;
  • 在EBX、ECX、EDX、ESI、EDI、EBP中设置系统调用参数(如果参数数量超过6个,则需特殊处理);
  • 调用中断服务int 0x80(系统调用都是通过中断服务的形式实现,int指令使得处理器从ring3切换到ring0,iret使处理器从ring0切换回ring3);
  • 系统调用的返回值通常保存在EAX中;

Linux下的系统调用定义在/usr/include/asm/unistd.h中,可以从中查看系统调用名称以及编号。下面是一个综合使用系统调用read、write、exit的例子,程序提示用户输入一个数字并读取用户输入,然后回显该数字。

section .data                           ;Data segment
   userMsg db 'Please enter a number: ' ;Ask the user to enter a number
   lenUserMsg equ $-userMsg             ;The length of the message
   dispMsg db 'You have entered: '
   lenDispMsg equ $-dispMsg                 

section .bss           ;Uninitialized data
   num resb 5
	
section .text          ;Code Segment
   global _start
	
_start:                ;User prompt
   mov eax, 4
   mov ebx, 1
   mov ecx, userMsg
   mov edx, lenUserMsg
   int 80h

   ;Read and store the user input
   mov eax, 3
   mov ebx, 2
   mov ecx, num  
   mov edx, 5          ;5 bytes (numeric, 1 for sign) of that information
   int 80h
	
   ;Output the message 'The entered number is: '
   mov eax, 4
   mov ebx, 1
   mov ecx, dispMsg
   mov edx, lenDispMsg
   int 80h  

   ;Output the number entered
   mov eax, 4
   mov ebx, 1
   mov ecx, num
   mov edx, 5
   int 80h  
    
   ; Exit code
   mov eax, 1
   mov ebx, 0
   int 80h

1.7 寻址模式

汇编语言中的寻址模式可以分为两类,一类是指令寻址,一类是数据寻址

  • 指令寻址:处理器要执行的指令如何寻址,分为顺序寻址(顺序执行,pc+=1)、跳跃寻址(jmp);
  • 数据寻址:根据数据所在存储位置的不同(内存或寄存器),以及地址提供方式的不同,数据寻址方式多种多样。

数据寻址方式虽然多样,但是都遵从如下指令格式:

|:–:|:–:|:–:| |操作码OP|寻址特征|形式地址A|

根据寻址特征以及形式地址A,可以计算出操作数的有效地址EA,不同的寻址特征对形式地址A施加的计算规则也不一样。下面总结一下常见的数据寻址方式。

  • 立即寻址,A是立即数(常量),EA=A;
  • 寄存器寻址,A是寄存器编号,EA=A=Ri;
  • 直接内存寻址,A为数据在内存中的有效地址,即EA=A;
  • 直接内存偏移量寻址,类似于基址寻址方式,EBX做基地址,A为offset;
  • 间接内存寻址,(A)为数据在内存中的有效地址,即EA=(A),间接内存寻址可能还会涉及到多重间址;

计算机组成原理中可能提到的数据寻址方式更加偏重于理论,寻址方式也更加多样,实际的汇编语言实现中可能并没有逐一实现,或者区分不明显,我们这里从实践出发,更加侧重于实用而不是理论,因此在使用术语的选择上也更加偏重于业内人员的偏好。

在汇编语言中,获取一个内存变量的有效地址的方式是:[varname]。

下面是一个综合使用了上述多种寻址方式的示例程序:

section	.text
   global_start     ;must be declared for linker (ld)
_start:             ;tell linker entry point
	
   ;writing the name 'Zara Ali'
   mov	edx,9       ;message length
   mov	ecx, name   ;message to write
   mov	ebx,1       ;file descriptor (stdout)
   mov	eax,4       ;system call number (sys_write)
   int	0x80        ;call kernel
	
   mov	[name],  dword 'Nuha'    ; Changed the name to Nuha Ali
	
   ;writing the name 'Nuha Ali'
   mov	edx,8       ;message length
   mov	ecx,name    ;message to write
   mov	ebx,1       ;file descriptor (stdout)
   mov	eax,4       ;system call number (sys_write)
   int	0x80        ;call kernel
	
   mov	eax,1       ;system call number (sys_exit)
   int	0x80        ;call kernel

section	.data
name db 'Zara Ali '

1.8 定义变量

汇编语言中提供了多个汇编器指令用于定义变量、预留内存空间。

section .data,为初始化的数据分配存储空间:

varname define-directive initial-value[,initial-value2]

常用的define-directive包括DB、DW、DD、DQ、DT,分别用于定义1byte、1word、1doubleword、1quadword、10bytesd并进行初始化操作。

section .bss,为未初始化的数据分配存储空间:

[varname] reserve-directive quantity

常用的reserve-directie指令包括RESB、RESW、RESD、RESQ、REST,分别用于分配1byte、1word、1doubleword、1quadword、10bytes的存储空间,结合操作数quantity可以计算出需要分配多少空间。

.bss节每一个对应的define-directive语句都有一个对应的reserve-directive与之对应,reserve-directive也可以单独存在,如下所示:

section .bss
	age db
	resb 1         ;只预留空间没关联变量
	num resb 1     ;预留空间并绑定变量

times directive允许多个变量初始化为相同的值:

varname times quantity define-directive [intiail-val]

注意times指令也可以用于.bss节,但是.bss节汇编的时候不会进行初始化,程序启动的时候才会进行0初始化。

  • 如果没有提供initial-val,会提示没有初始值不进行初始化;
  • 如果提供了intiail-val,会提示.bss节忽略了初始值不进行初始化操作;

1.9 定义常量

汇编语言中定义常量的指令包括3个,分别是EQU、%assign、%define。

const-name EQU value ;可以定义字符串或者数值常量
%assign const-name value ;只可以定义数值常量,可以重定义
%define const-name value ;可以定义字符串或者数值常量

前面曾多次使用EQU进行常量定义,这里就不再提供其他示例程序了。

1.10 算术指令

汇编语言中的算术运算指令包括:

  • INC、DEC,自增、自减一个寄存器或者内存变量的值,结果保存到当前操作数;
  • ADD、SUB,加、减一个寄存器或者内存变量的值,结果保存到第一个操作数;
  • MUL、IMUL,分别处理无符号、有符号数的乘法,保存存储遵循如下规则:
    • 8位乘法,如:AL * 8bit_source = AH AL,结果高8位保存到AH、低8位保存到AL;
    • 16位乘法,如:AX * 16bit_source = DX AX,结果高16位保存到DX,低16位保存到AX;
    • 32位乘法,如:EAX * 32bit_source = EDX EAX,结果高32位保存到EDX,低32位保存到EAX;
  • DIV、IDIV,分别处理无符号、有符号数的除法,结果存储遵循如下规则:
    • 16位除法,如:AX / 8bit_source = AL…AH,商保存到AL,余数保存到AH;
    • 32位除法,如:DX AX / 16bit_source = AX…DX,被除数高16位在DX、低16位在AX,结果商在AX、余数在DX;
    • 64位除法,如:EDX EAX / 32bit_source = EAX…EDX,被除数高32位在EAX、低32位在EAX,结果商在EAX、余数在EDX;

1.11 逻辑指令

汇编语言中的逻辑运算指令包括:

  • AND,逻辑与运算,结果保存到第一个操作数;
  • OR,逻辑或运算,结果保存到第一个操作数;
  • NOT,对当前操作数求反,结果保存到当前操作数;
  • XOR,异或运算,结果保存到第一个操作数;
  • TEST,测试运算,不会改变操作数的值,但运算会影响ZF标识;

1.12 分支控制

通过某些循环、分支指令可以实现分支语句,这里对应的汇编指令主要都是基于处理器中的标识寄存器来实现的。

常用指令包括:

  • 比较指令
    CMP,比较两个操作数是否相同、谁大谁小,需结合其他条件转移指令使用;
  • 无条件转移指令
    JMP,无条件跳转到制定的指令地址处执行;
  • 条件转移指令 有符号数、无符号数通用的包括:JE/JZ,JNE/JNZ; 无符号数特有的包括:JG、JGE、JL、JLE等; 有符号数特有的包括:JA、JAE、JB、JBE等; 特殊用途的包括:JC、JNC、JO、JNO、JS、JNS等;

这里涉及到的条件转移指令比较多,这里不一一进行描述了,有需要的话读者朋友可以参考”分支控制指令“,或者可以参考intel指令集了解更多的细节。

1.13 循环控制

借助条件转移指令实现循环

条件转移指令可以用于实现循环控制,循环控制次数可以存储在ECX寄存器中,循环体内动作每执行一次将ECX值减1,根据ECX值是否为0决定是否进行循环。下面就是一个根据这个简单思路实现的循环体:

MOV	CL, 10
L1:
	<LOOP-BODY>
DEC	CL
JNZ	L1

借助内置的loop指令实现循环

汇编语言内部提供了指令loop来实现循环,起实现方式跟我们上面说的是一样的,loop指令会检查当前ECX寄存器的值是否为0,为0则退出循环,大于0则执行DEC ECX并继续执行循环体。

下面是一个借助loop指令实现的循环体版本,书写上也更加简练:

MOV	CL, 10
L1:
	<LOOP-BODY>
loop L1

1.14 数字

前面我们读入一个数位数值的时候需要将其减去'0’之后得到其真实数值,运算结果写出之前也需要将数位数值加上'0’再写出,为啥?这里涉及到ASCII码与数值之间的转换。

上述处理方式虽然比较直观,但是负载比较大,汇编语言中有更加高效的处理方式,即以二进制形式对其进行处理。

十进制数字有两种表示形式:

  • ASCII码形式
    输入的十进制数字每个数位都用ASCII来表示,十进制数字1234的4个数位分别被编码为对应的ASCII码字符,各个字符对应的十进制值分别为:31 32 33 34,共占用了4个字节;
  • BCD码形式
    • 如果是unpacked BCD编码形式,输入的十进制数字每个数位都用1字节的二进制形式来表示,十进制数字1234的4个数位分别被编码为:01 02 03 04,共占用4个字节。
    • 如果是packed BCD编码形式,输入的十进制数字每个数位用4bit来表示,十进制数字1234的4个数位被编码为:12 34,共占用2个字节。

运算完成之后,可能会涉及到某些ASCII、BCD码之间的转换动作,可以借助于对应的汇编调整指令来实现。

1.15 字符串

计算字符串长度

前面我们指定一个字符串的长度的时候,可以通过变量来显示地指明,也可以通过**”$-msg“**来计算出来,使用后者的时候我们需要为msg字符串尾部添加一个哨兵字符,例如:

msg db 'hello world',0xa
len db $-msg

$代表的是当前的offset,offset-db正好是msg中字符的数量,不包括0xa,如果不添加0xa这个字符哨兵的话,len就应该定义成**"$-msg+1”**。

字符串操作指令:

  • MOVS,移动一个字符串;
  • LODS,从内存中装载字符串;
  • STOS,存储字符串到内存;
  • CMPS,比较字符串;
  • SCAS,比较寄存器和内存中的字符串;
  • REP/REPZ/REPNZ,便利字符串并针对各个字符重复执行某个操作;

1.16 数组

定义数组,主要有如下几种方式,我们以定义一个byte数组为例分别说明。

定义数组方式一:

numbers db 0,1,2,3,4,5

定义数组方式二:

numbers db 0
        db 1
        db 2
        db 3
        db 4
        db 5

这种方式应该比较少用,方式一其实是这种方式的简化版。

定义数组方式三:

numbers times 6 db 0

这种方式定义的6个byte都被初始化为相同的值0。

还是要根据自己的需要来选择合适的数组定义方式。

下面示例程序定义了一个数组byte数组x,然后以循环的形式遍历x中的元素并求和:

section	.text
   global _start   ;must be declared for linker (ld)
	
_start:	 		
   mov  eax,3      ;number bytes to be summed 
   mov  ebx,0      ;EBX will store the sum
   mov  ecx, x     ;ECX will point to the current element to be summed

top:  add  ebx, [ecx]
   add  ecx,1      ;move pointer to next element
   dec  eax        ;decrement counter
   jnz  top        ;if counter not 0, then loop again

done: 
   add   ebx, '0'
   mov  [sum], ebx ;done, store result in "sum"

display:
   mov  edx,1      ;message length
   mov  ecx, sum   ;message to write
   mov  ebx, 1     ;file descriptor (stdout)
   mov  eax, 4     ;system call number (sys_write)
   int  0x80       ;call kernel
	
   mov  eax, 1     ;system call number (sys_exit)
   int  0x80       ;call kernel

section	.data
global x
x:    
   db  2
   db  4
   db  3

sum: 
   db  0

1.17 函数

汇编语言中函数是非常重要的一个组成部分,定义函数的语法如下:

function_name:
	<function_body>
	ret

程序中调用一个函数的指令为call:

call <function_name>

下面示例代码总定义了一个求和函数:

section	.text
   global _start        ;must be declared for using gcc
	
_start:	                ;tell linker entry point
   mov	ecx,'4'
   sub     ecx, '0'
	
   mov 	edx, '5'
   sub     edx, '0'
	
   call    sum          ;call sum procedure
   mov 	[res], eax
   mov	ecx, msg	
   mov	edx, len
   mov	ebx,1	        ;file descriptor (stdout)
   mov	eax,4	        ;system call number (sys_write)
   int	0x80	        ;call kernel
	
   mov	ecx, res
   mov	edx, 1
   mov	ebx, 1	        ;file descriptor (stdout)
   mov	eax, 4	        ;system call number (sys_write)
   int	0x80	        ;call kernel
	
   mov	eax,1	        ;system call number (sys_exit)
   int	0x80	        ;call kernel
sum:
   mov     eax, ecx
   add     eax, edx
   add     eax, '0'
   ret
	
section .data
msg db "The sum is:", 0xA,0xD 
len equ $- msg   

segment .bss
res resb 1

栈stack,是一种后进先出LIFO的数据结构,汇编语言提供了指令push、pop来进行入栈、出栈操作。

1.18 递归

递归操作,指的是一个函数func在执行过程中会调用这个函数自身的情况。递归又可以细分为直接递归和间接递归。

  • 直接递归,函数func的函数体中会调用自身;
  • 间接递归,函数func的函数体中调用了其他的函数,而这个被调用的函数中又调用了函数func;

有些问题适合用递归算法来解决,递归比较容易理解,但是对栈空间消耗可能会超出系统允许的上限导致栈溢出问题,此时需要将递归算法转换为非递归算法。

1.19 宏

汇编语言中可以将常用的、可能多次重复使用的指令序列以宏macro的形式进行封装,在程序中可以多次调用。

定义宏的形式为:

%macro macro_name params_quantity
	<macro_body>
%endmacro

调用宏的形式为:

macro_name param1, param2

下面的示例程序中,将输出字符串的指令序列以宏的形式进行了封装:

; A macro with two parameters
; Implements the write system call
   %macro write_string 2 
      mov   eax, 4          ;sys_write
      mov   ebx, 1          ;stdout
      mov   ecx, %1         ;param1, buf
      mov   edx, %2         ;param2, buf_len
      int   80h             ;call kernel
   %endmacro
 
section	.text
   global _start            ;must be declared for using gcc
	
_start:                     ;tell linker entry point
   write_string msg1, len1               
   write_string msg2, len2    
   write_string msg3, len3  
	
   mov eax,1                ;sys_exit
   int 0x80                 ;call kernel

section	.data
msg1 db	'Hello, programmers!',0xA,0xD 	
len1 equ $ - msg1			

msg2 db 'Welcome to the world of,', 0xA,0xD 
len2 equ $- msg2 

msg3 db 'Linux assembly programming! '
len3 equ $- msg3

1.20 文件操作

Linux内核提供了一系列文件操作的系统调用,常用的几个系统调用如下:

  • sys_open
  • sys_close
  • sys_creat
  • sys_read
  • sys_write
  • sys_lseek

系统调用编号可以在/usr/include/asm/unistd.h中检查到,系统调用参数、返回值信息可以借助Linux man手册进行查询。

汇编语言里面对于上述系统调用的调用与前面sys_read、sys_write示例程序中的使用方式是一致的,都按照如下几个步骤进行调用:

  • 将系统调用的编号设置到EAX;
  • 将系统调用的参数依次设置到EBX、ECX、EDX、ESI、EDI、EBX;
  • 触发内核中断int 80h;
  • 检查EAX中保存的系统调用返回值;

如下示例程序对文件相关的系统调用进行了组合使用,首先创建一个文件并写入数据,然后关闭,再重新打开文件并读取文件内容,最后在stdout上打印文件内容。

section	.text
   global _start         ;must be declared for using gcc
	
_start:                  ;tell linker entry point
   ;create the file
   mov  eax, 8
   mov  ebx, file_name
   mov  ecx, 0777        ;read, write and execute by all
   int  0x80             ;call kernel
	
   mov [fd_out], eax
    
   ; write into the file
   mov	edx,len          ;number of bytes
   mov	ecx, msg         ;message to write
   mov	ebx, [fd_out]    ;file descriptor 
   mov	eax,4            ;system call number (sys_write)
   int	0x80             ;call kernel
	
   ; close the file
   mov eax, 6
   mov ebx, [fd_out]
    
   ; write the message indicating end of file write
   mov eax, 4
   mov ebx, 1
   mov ecx, msg_done
   mov edx, len_done
   int  0x80
    
   ;open the file for reading
   mov eax, 5
   mov ebx, file_name
   mov ecx, 0             ;for read only access
   mov edx, 0777          ;read, write and execute by all
   int  0x80
	
   mov  [fd_in], eax
    
   ;read from file
   mov eax, 3
   mov ebx, [fd_in]
   mov ecx, info
   mov edx, 26
   int 0x80
    
   ; close the file
   mov eax, 6
   mov ebx, [fd_in]
    
   ; print the info 
   mov eax, 4
   mov ebx, 1
   mov ecx, info
   mov edx, 26
   int 0x80
       
   mov	eax,1             ;system call number (sys_exit)
   int	0x80              ;call kernel

section	.data
file_name db 'myfile.txt'
msg db 'Welcome to Tutorials Point'
len equ  $-msg

msg_done db 'Written to file', 0xa
len_done equ $-msg_done

section .bss
fd_out resb 1
fd_in  resb 1
info resb  26

1.21 内存管理

Linux内核提供了系统调用sys_brk来分配堆内存区域,sys_brk实际上是增加了进程最大可动态申请的内存地址的上限,brk分配的内存区域(堆)仅仅挨着.data节,系统调用sys_brk参数为0时会返回当前可申请内存的最大地址,参数不为0时会调整当前brk边界。

下面的示例程序通过系统调用sys_brk来动态分配了16KB的内存空间:

section	.text
   global _start         ;must be declared for using gcc
	
_start:	                 ;tell linker entry point

   mov	eax, 45		 ;sys_brk
   xor	ebx, ebx
   int	80h

   add	eax, 16384	 ;number of bytes to be reserved
   mov	ebx, eax
   mov	eax, 45		 ;sys_brk
   int	80h
	
   cmp	eax, 0
   jl	exit	;exit, if error 
   mov	edi, eax	 ;EDI = highest available address
   sub	edi, 4		 ;pointing to the last DWORD  
   mov	ecx, 4096	 ;number of DWORDs allocated
   xor	eax, eax	 ;clear eax
   std			 ;backward
   rep	stosd            ;repete for entire allocated area
   cld			 ;put DF flag to normal state
	
   mov	eax, 4
   mov	ebx, 1
   mov	ecx, msg
   mov	edx, len
   int	80h		 ;print a message

exit:
   mov	eax, 1
   xor	ebx, ebx
   int	80h
	
section	.data
msg    	db	"Allocated 16 kb of memory!", 10
len     equ	$ - msg

1.22 总结

这里结合tutorialspoint上的汇编语言教程对相关的知识点进行了简要回顾,也有所收获,这里也分享给需要的同学。