NASM汇编语言基础

编写helloworld程序

为了在屏幕上输出指定内容,我们需要调用操作系统内核提供的函数,调用系统调用时,内核将立即暂停程序的执行。然后,它将联系执行硬件上请求的任务 所需的必要驱动程序,然后将控制返回给程序。

注:驱动程序(Drivers)之所以这么称呼是因为内核实际上使用它们来驱动硬件

我们可以通过加载要执行的函数编号(操作代码 OPCODE)并在其余寄存器中填充要传递给系统调用的参数来完成这一切。可以使用 INT 指令请求软件中断,内核接管并调用库中的函数,并采用我们的参数。

例如,当 EAX 设定为 1 将调用 sys_exit 时请求中断,当 EAX 设置为 4 将调用 sys_write 时请求中断。如果函数需要参数 EBX,ECX,EDX 将作为参数传递。

首先,在 .data 部分(SECTION .data)中创建一个变量"msg",并为其分配想要在此例中输出的字符串 “HELLOWORLD!!!”,然后使用 ($ - msg 作为以上字符串的长度)。

在 .text 部分(SECTION .text)中,通过向内核提供全局标签 _start:来表示程序入口点来告诉内核从何处开始执行。

使用系统内核调用 sys_write 将消息输出到控制台窗口。此功能在linux系统中使用操作码为4。该函数还采用3个参数,这些参数在请求将执行任务的软件中断之前,按顺序加载到EDXECX和EBX几个寄存器中。

  • EDX:含有字符串的长度信息(单位:Byte)
  • ECX:含有字符串的地址(指向字符串的指针)
  • EBX:文件输出(此例中是标准输出 STDOUT 到屏幕,也可以输出到文件)
 ;hello_world.asm
 ;在屏幕(标准输出)中显示"HelloWorld!!!"
 ;第一版
 
 SECTION .data
 msg db "HelloWorld!!!" , 0ah , 0dh  ;0ah(ASCII码:换行符),odh(ASCII码:回车符)
 msglen equ ($ - msg)
 
 SECTION .text
 global _start
 
 _start:
     mov edx,msglen
     mov ecx,msg
     mov ebx,1d
     mov eax,4d
     int 80h

码确实跑起来了,但是仍然会报错:

 Segmentation fault

这是因为程序还不完整,还缺少一个合适的退出。计算机程序可以被认为是一长串指令,加载到内存中,并分为多个部分(或段)。此常规内存池在所有程序之间共享,可用于存储变量、指令、其他程序或任何真正内容。每个段都指定一个地址,以便以后可以找到存储在该部分中的信息。要执行加载到内存中的程序,我们使用全局标签_start: 告诉操作系统在内存中可以找到和执行我们的程序的位置。然后按照程序逻辑按顺序访问内存,该逻辑确定要访问的下一个地址。操作系统的内核跳转到内存中的该地址并执行它。

请务必告诉操作系统应从何处开始执行,以及应该停止何处。在之前程序中,我们没有告诉内核在哪里停止执行。因此,在调用sys_write之后,程序继续按顺序执行内存中的下一个地址,我们不知道内核试图执行什么,但它导致其终止了我们的进程, 留下了"Segmentation faul"的错误消息。在所有程序的末尾调用 sys_exit 将意味着内核确切知道何时终止进程并将内存返回到常规池,从而避免错误。

在调用 sys_exit 时,需要运用到eax与ebx寄存器:

  • EBX:包含一个程序正常退出时返回的状态码,一般使用 “0” ,此处代表没有错误出现
  • EAX:置1,来代表调用的是 sys_exit 函数
  • 最后使用 int 80h来完成调用
 ;hello_world.asm
 ;在屏幕(标准输出)中显示"HelloWorld!!!"
 ;第二版
 
 SECTION .data
 msg db "HelloWorld!!!" , 0ah , 0dh  ;0ah(ASCII码:换行符),odh(ASCII码:回车符)
 msglen equ ($ - msg)
 
 SECTION .text
 global _start
 
 _start:
     mov edx,msglen
     mov ecx,msg
     mov ebx,1d
     mov eax,4d
     int 80h  ;调用linux系统中断,在屏幕上输出字符串
 
     mov ebx,0d  ;表示正常退出
     mov eax,1d
     int 80h  ;调用 SYS_EXIT ,正常退出

计算字符串的长度

当我们不清楚到底这个字符串的长度是多少的时候,就需要一种方法来在程序运行时自动确定它的长度

为了计算字符串的长度,我们将使用一种称为指针算术的技术。两个寄存器初始化,指向内存中的同一地址。一个寄存器(在本例中为 EAX)将向前递增一个字节,直到我们到达字符串的末尾。然后将从 EAX 中减去原始指针。这实际上类似于两个数组之间的减法,结果生成两个地址之间的元素数。然后,此结果将传递给sys_write,以替换我们的硬编码计数

CMP 指令将左侧与右侧进行比较,并设置用于程序流的许多标志。我们要检查的标志是 ZF (zero flag) ,零标志位。当 EAX 指向的字节等于零时,将设置 ZF 标志。然后,我们使用 JZ 指令跳转到程序中标记为"已完成"的点(如果设置了 ZF 标志)。这是要从下一个char循环中断,并继续执行程序的其余部分

 ;hello_len.asm
 ;自动字符串长度
 ;使用 nasm -f elf hello_len.asm 汇编
 ;使用 ld -m elf_i386 hello_len.o -o hello_len 链接
 ;使用 ./hello_len 运行
 
 SECTION .data
 msg db "hhhhhhhhhhhhhhhhhhhelloworld!!!!!!!!", 0ah, 0dh, 0h ;字符串可以修改,同时确保程序的正确性(字符串以 ‘0’ 结尾)
 
 SECTION .text
 global _start
 
 _start:
     mov eax,msg ;将msg的初地址复制到eax
     mov ebx,eax    ;将msg的初地址复制到ebx
 
 nextchar:
     cmp byte [eax], 0 ;比较eax现在所指向的字符 与 零,(零是字符串的结尾)
         ;如 cmp oprd1,oprd2
         ;为第一个数减去第二个数
         ;但是不影响两个操作数的值
         ;它影响flag的 CF、ZF、OF、AF、PF 标志位
         ;若ZF == 1,则两个数相等
         ;当无符号时
         ;若CF==1,则出现了借位,即oprd1 < oprd2
         ;若CF==0,且ZF!=1,则oprd1 > oprd2
 
     jz finished
     inc eax
     jmp nextchar
 
 finished:
     sub eax,ebx ;得到字符串长度,并且保存在eax中
 
     mov edx,eax
     mov ecx,msg
     mov ebx,1
     mov eax,4
     int 80h
 
     mov ebx,0
     mov eax,1
     int 80h

子程序(subroutines)

子程序(又称子例程)相当于函数。它们是可重用的代码段,程序可以调用它们来执行各种可重复的任务。子例程使用标签声明,就像我们以前使用过的标签一样(例如,_start:)但是,我们不使用JMP指令来调用它们 —— 而是使用新的指令CALL。在运行函数后,我们也不会使用 JMP 指令返回到我们的程序。为了从子程序返回到我们的程序,我们改用指令RET。

函数需要使用到的任何寄存器都应该将其当前的值存放到堆栈上,我们使用 PUSH 指令对其进行安全保存。然后,在函数完成执行之后,可以使用 POP 指令还原这些寄存器的原始值。这意味着寄存器中的任何值在调用函数之前与之后都是相同的。如果我们在子程序中处理这一点,我们可以调用函数,而不必担心它们对我们的寄存器进行了哪些更改。

CALL 和 RET 指令也使用堆栈。调用函数时,从调用它时 的地址将被 PUSH 到堆栈上。然后,RET 会弹出此地址,程序将跳回代码中的位置。

 ;hello.asm
 ;子例程版本
 ;使用 nasm -f elf hello.asm 汇编
 ;使用 ld -m elf_i386 hello.o -o hello 链接
 ;使用 ./hello 运行
 
 SECTION .data
 msg db "hello, ASSWECAN !!!", 0ah, 0dh, 0
 
 SECTION .text
 global _start
 
 _start:
 
     mov eax, msg ;把 msg 的初始地址存入eax寄存器
     call strlen ;调用函数 strlen 来计算字符串长度
   
     mov edx, eax ;strlen 函数将结果存放在了eax寄存器中
     mov ecx, msg ;接下来的与上个程序相似
     mov ebx, 1
     mov eax, 4
     int 80h
 
     mov ebx, 0
     mov eax, 1
     int 80h
 
 strlen:            ;这是我们编写的第一个函数声明
     push ebx    ;我们将ebx的数据保存到堆栈中,这样它就不会被函数改变
     mov ebx, eax;与上一个程序相似
 
 nextchar:
     cmp byte [eax], 0
     jz finished
     inc eax
     jmp nextchar
 
 finished:
     sub eax, ebx
     pop ebx        ;将堆栈中保存的ebx值返回到ebx寄存器中
     ret            ;返回调用函数的地方

外部包含文件

外部包含文件允许我们移动代码,并将其放入单独的文件中。此项技术可用于编写整洁并且易于维护的程序。可重用的代码 可以编写为子例程,并存储在称为库的单独文件中。当你需要调用它们时,可以将该文件包含在程序中,并使用该文件。

functions.asm

 ;----------------------------------
 ;int slen(char*)
 ;计算字符串长度
 slen:
     push ebx
     mov ebx, eax
 nextchar:
     cmp byte [eax], 0
     jz finished
     inc eax
     jmp nextchar
 finished:
     sub eax, ebx
     pop ebx
     ret
 
 ;----------------------------------
 ;void sprint(char*)
 ;打印字符串
 sprint:
     push edx
     push ecx
     push ebx
     push eax
     call slen
     mov edx, eax
     pop eax
     mov ecx, eax
     mov ebx, 1
     mov eax, 4
     int 80h
     pop ebx
     pop ecx
     pop edx
     ret
 
 ;----------------------------------
 ;void exit()
 ;退出程序
 quit:
     mov ebx, 0
     mov eax, 1
     int 80h
     ret

test.asm

 ;test.asm
 
 %include    'functions.asm'            ;引用我们的 functions.asm
 
 SECTION .data
 msg1 db "helloworld!!!", 0ah, 0dh
 msg2 db "This is how we recycle in NASM", 0ah, 0dh
 
 SECTION .text
 global _start
 _start:
     mov eax, msg1
     call sprint
 
     mov eax, msg2
     call sprint
     call quit

NULL终止字节

外部包含程序中的程序是存在问题的:第二条消息输出了两次

实际发生的事情是,我们没有适当地终止我们的字符串。在程序集中,变量是一个接一个地存储在内存中,因此msg1变量的最后一个字节就在msg2变量的第一个字节旁边。我们知道我们的slen函数正在寻找一个零字节,所以除非我们的msg2变量以零字节开头,否则它一直在计数,就好像它是同一个字符串一样(就程序集而言,它们是同一个字符串)。因此,我们需要在字符串之后放置一个零字节或者说0h,以便程序集知道在哪里停止计数。

 ;hello3.asm
 
 %include 'functions.asm'
 
 SECTION .data
 msg1 db "hello !!!!", 0ah, 0dh, 0h
 msg2 db "this is invalid!!!", 0ah, 0dh, 0h
 
 SECTION .text
 global _start:
 _start:
     mov eax, msg1
     call sprint
 
     mov eax, msg2
     call sprint
 
     call quit

换行

换行操作对于控制台程序(如我们的"helloworld"程序)而言,至关重要。一旦我们开始构建需要用户输入的程序,它们就变得更加重要。但换行操作有时会让我们头疼。有时,我们需要将它们包含在字符串中,有时则希望删除它们。如果我们通过在声明的消息文本后添加 0Ah 来继续在变量中硬编码它们,那将成为一个问题。如果代码中有一个位置,我们不想打印出该变量的换行符,我们需要编写一些额外的逻辑,在运行时从字符串中删除它。

如果我们编写一个函数来打印消息,然后打印换行符,则可以提高程序的可维护性。这样,当我们需要换行时,我们可以调用此函数,而不需要时调用之前 sprint 函数。

对sys_write的调用需要将指向要打印的字符串的指针传递给它,这样我们也不能只是将换行符(0Ah)传递给打印函数。我们也不想创建另一个变量只是为了保留换行符,所以我们将改为使用堆栈。

slen:    ;修改后的functions.asm
    push ebx
    mov ebx, eax
nextchar:
    cmp byte [eax], 0
    jz finished
    inc eax
    jmp nextchar
finished:
    sub eax, ebx
    pop ebx
    ret

sprint:
    push edx
    push ecx
    push ebx
    push eax
    call slen
    mov edx, eax
    pop eax
    mov ecx, eax
    mov ebx, 1
    mov eax, 4
    int 80h
    pop ebx
    pop ecx
    pop edx
    ret

sprintLF:    ;新增的函数,实现自动换行
    call sprint
    push eax
    mov eax, 0ah
    push eax
    mov eax, esp
    call sprint
    pop eax
    pop eax
    ret

quit:
    mov ebx, 0
    mov eax, 1
    int 80h
    ret
;test.asm

%include 'functions.asm'

SECTION .data
msg1 db 'hello !!!!!', 0h
msg2 db 'asswecan', 0h

SECTION .text
global _start
_start:
    mov eax, msg1
    call sprintLF
    mov eax, msg2
    call sprintLF
    call quit

用户输入

关于 SECTION .bss 的简介:

到目前为止,我们已经使用了.text和.data,现在我们即将引入.bss了。BSS 代表着Block Started by Symbol。它是我们程序中的一块区域,用于为未初始化的变量在内存中保留空间。我们将使用它在内存中保留一些空间用来保存我们的用户输入,我们不必知道需要存储多少字节。

使用如下:

SECTION .bss
variableName1: RESB 1 ; 为一个字节保留(reserve)的空间
variableName2: RESW 1 ; 为一个字保留(reserve)的空间
variableName3: RESD 1 ; 为一个双字保留(reserve)的空间
variableName4: RESQ 1 ; 为一个双精度浮点值保留(reserve)的空间
variableName5: REST 1 ; 为一个拓展精度浮点值保留(reserve)的空间

使用系统调用sys_read接收和处理来自用户的输入。它在Linux系统调用表中为OPCODE 3。就像sys_write一样,此函数也采用3个参数,这些参数将在请求将调用该函数的软件中断之前加载到EDX、ECX和EBX中。

  • EDX为缓冲区大小(以字节为单位)
  • ECX为在.bss部分中创建的变量的地址(变量名)
  • EBX为我们想要写入的文件,在这种情况下为 STDIN(标准输入,键盘)

与往常一样,传递的参数的数据类型和含义可以在函数的定义中找到。

 ;input.asm
 
 %include 'functions.asm'
 
 SECTION .bss
 name RESB 255
 
 SECTION .data
 msg1 db "hello,what'syourname:", 0h
 msg2 db "hello, ", 0h
 msg3 db "hello!!", 0h
 msg4 db "areyouok??", 0h
 
 SECTION .text
 global _start
 _start:
     mov eax, msg1
     call sprint
     mov edx, 255
     mov ecx, name
     mov ebx, 0
     mov eax, 3
     int 80h
     mov eax, msg2
     call sprint
     mov eax, name
     call sprint
     mov eax, msg3
     call sprintLF
     mov eax, msg4
     call sprintLF
     call quit
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《x86汇编语言:从实模式到保护模式》主要讲述INTEL x86处理器的16位实模式、32位保护模式,至于虚拟8086模式,则是为了兼容传统的8086程序,现在看来已经完全过时,不再进行讲述。《x86汇编语言:从实模式到保护模式》的特色之一是提供了大量典型的源代码,这些代码以及相配套的工具程序可以到书中指定的网站,或者电子工业出版社华信教育资源网搜索下载。 每一种处理器都有它自己的机器指令集,而汇编语言的发明则是为了方便这些机器指令的记忆和书写。尽管汇编语言已经较少用于大型软件程序的开发,但从学习者的角度来看,要想真正理解计算机的工作原理,掌握它内部的运行机制,学习汇编语言是必不可少的。本书采用开源的NASM汇编语言编译器和VirtualBox虚拟机软件,以个人计算机广泛采用的Intel处理器为基础,详细讲解了Intel处理器的指令系统和工作模式,以大量的代码演示了16/32/64位软件的开发方法,介绍了处理器的16位实模式和32位保护模式,以及基本的指令系统。这是一本有趣的书,它没有把篇幅花在计算一些枯燥的数学题上。相反,它教你如何直接控制硬件,在不借助于BIOS、DOS、Windows、Linux或者任何其他软件支持的情况下来显示字符、读取硬盘数据、控制其他硬件等。本书可作为大专院校相关专业学生和计算机编程爱好者的教程
第1章 基本知识 1-1 汇编语言介绍 1-1-1 程序设计语言分类 1-1-2 汇编语言程序设计的意义 1-2 位及字节 1-3 二进制数 1-3-1 数字系统 1-3-2 补码 1-3-3 BCD码 1-4 十六进制表示法 1-5 ASCII码 1-6 个人计算机组成 1-6-1 处理器 1-6-2 内部存储器 1-6-3 段与地址 1-6-4 寄存器 1-7 硬件中断 课后习题 第2章 程序加载并执行 2-1 操作系统的组成 2-2 BIOS启动程序 2-3 系统加载程序 2-4 堆栈 课后习题 第3章 NASM汇编语言基础 3-1 源程序行格式 3-2 伪指令 3-2-1 定义含有初值的数据 3-2-2 定义不含初值的数据 3-2-3 INCBIN伪指令 3-2-4 EQU伪指令 3-2-5 TIMES伪指令 3-3 有效地址 3-4 常量 3-4-1 数字常量 3-4-2 字符常量 3-4-3 字符串常量 3-4-4 浮点数常量 3-5 表达式 3-5-1 OR运算符 3-5-2 XOR运算符 3-5-3 AND运算符 3-5-4 移位运算符 3-5-5 加及减运算符 3-5-6 乘及除运算符 3-5-7 单元运算符 3-6 临界表达式 3-7 局部标号 3-8 预处理器 3-8-1 %define指令 3-8-2 %undef指令 3-8-3 %assign指令 3-8-4 多行宏 3-8-5 条件汇编 3-8-6 预处理循环 3-8-7 文件引用指引 3-8-8 标准宏 3-8-9 汇编语言指引 3-9 目标文件格式 3-10 NASM汇编程序安装 3-11 范例 课后习题 第4章 一般指令 4-1 源操作数与目的操作数 4-2 MOV传送指令 4-3 XCHG互换指令 4-4 有效地址送寄存器指令LEA 4-5 指针送寄存器指令LDS及LES指令 4-6 压入PUSH及弹出POP指令 4-7 存储寄存器PUSHA及POPA指令 4-8 标志寄存器传送PUSHF及POPF指令 4-9 没有运算的NOP指令 课后习题 第5章 基本输入与输出 5-1 软件中断INT指令 5-2 将一个字符串输出到屏幕 5-3 从键盘输入一个字符 5-4 将一个字符输出到屏幕 5-5 从键盘输入一个字符串 5-6 将一个字输出到屏幕 5-7 显示内存内容 5-8 键盘输入控制 5-8-1 由键盘输入字符 5-8-2 直接由键盘输入或输出字符 5-8-3 直接由键盘输入字符 5-8-4 直接由键盘输入字符 5-8-5 由键盘输入字符串 5-8-6 检查键盘缓冲区 5-8-7 清除键盘缓冲区 5-8-8 从键盘缓冲区读取字符 5-8-9 测试键盘缓冲区是否有字符 5-8-10 传回控制键状态 5-9 屏幕输出控制 5-9-1 显示字符 5-9-2 显示字符串 5-9-3 设定光标位置 5-9-4 向上滚动屏幕 5-10 打印机输出控制 5-10-1 输出字符至打印机 5-10-2 打印一个字符 5-10-3 取得打印机状态 课后习题 第6章 程序流程控制 6-1 标志寄存器 6-2 改变标志的指令 6-3 条件转移指令 6-4 比较两个整数 6-5 无条件转移指令JMP 6-6 循环指令LOOP 6-7 选择结构 6-8 循环结构 课后习题 第7章 算术运算 7-1 定点数与浮点数 7-2 带符号及无符号整数 7-3 加法及减法 7-4 乘法 7-5 除法 7-6 BCD十进制数运算 7-6-1 BCD加法 7-6-2 BCD减法 7-6-3 BCD乘法 7-6-4 BCD除法 7-6-5 BCD宏应用 7-7 综合例题 课后习题 第8章 宏 8-1 单行宏 8-1-1 %define指令 8-1-2 %undef指令 8-1-3 %assign指令 8-2 多行宏 8-2-1 显示字符串宏 8-2-2 显示字符宏 8-2-3 读取字符宏 8-2-4 显示字节宏 8-2-5 读取字符串宏 8-2-6 字符串转换为数值 8-2-7 数值转换为字符串 8-2-8 数值输出至屏幕 8-3 条件汇编 8-4 预处理循环 8-5 源程序文件的包含内容 8-6 相关宏汇总 课后习题 第9章 过程 9-1 过程的定义 9-2 过程里的局部变量 9-3 传值调用 9-4 传址调用 9-5 堆栈传递参数 9-6 内存传递参数 课后习题 第10章 字符串处理 10-1 声明字符串 10-2 字符串长度 10-3 基本字符串指令 10-4 转换指令XLATB 10-5 字符串宏 课后习题 第11章 位运算 11-1 位基本运算 11-2 位屏蔽 11-3 AND指令 11-4 OR指令 11-5 XOR指令 11-6 NOT指令 11-7 TEST指令 11-8 改变位位置 11-9 左移及右移 11-10 算术左移及算术右移 11-11 循环位移 11-12 位移及循环位移指令总结 11-13 综合例题 课后习题 第12章 文件处理 12-1 输入及输出层次 12-2 输入及输出概念 12-3 标准的文件代号 12-4 建立一个文件代号 12-5 打开一个文件 12-6 关闭一个文件 12-7 从文件或设备读取数据 12-8 数据写入文件或设备 12-9 移动文件指针 12-10 检查并修改文件属性 12-11 建立新文件 12-12 删除文件 12-13 文件改名 12-14 建立或删除子目录 12-15 取得当前目录 12-16 改变当前目录 12-17 取得缺省的磁盘驱动器 12-18 改变缺省的磁盘驱动器 12-19 低级输入及输出 课后习题 第13章 数据结构 13-1 数组声明 13-2 数组查找 13-3 使用XLATB指令转换 13-4 排序 13-5 队列 13-6 堆栈 13-7 链表 课后习题 第14章 浮点数运算 14-1 80x87协处理器的运算 14-2 浮点堆栈 14-3 状态字 14-4 控制字 14-5 数据类型 14-5-1 二进制整数 14-5-2 聚集十进制数 14-5-3 实数 14-5-4 七种数据类型值的范围 14-6 80x87指令集 14-7 范例 课后习题 第15章 连接程序 15-1 建立NASM源程序 15-2 将目标文件连接成.exe文件 15-3 显示DOS的BIOS区域数据 15-4 系统设备数据 15-5 内存容量 课后习题 附录 NASM汇编语言指令

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值