先上一段代码示例:

  1. ; Hello World Program (Calculating string length)
  2. ; Compile with: nasm -f elf helloworld-len.asm
  3. ; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-len.o -o helloworld-len
  4. ; Run with: ./helloworld-len
  5. SECTION .data
  6. msg db 'Hello, brave new world!', 0Ah ; we can modify this now without having to update anywhere else in the program
  7. SECTION .text
  8. global _start
  9. _start:
  10. mov ebx, msg ; move the address of our message string into EBX
  11. mov eax, ebx ; move the address in EBX into EAX as well (Both now point to the same segment in memory)
  12. nextchar:
  13. cmp byte [eax], 0 ; compare the byte pointed to by EAX at this address against zero (Zero is an end of string delimiter)
  14. jz finished ; jump (if the zero flagged has been set) to the point in the code labeled 'finished'
  15. inc eax ; increment the address in EAX by one byte (if the zero flagged has NOT been set)
  16. jmp nextchar ; jump to the point in the code labeled 'nextchar'
  17. finished:
  18. sub eax, ebx ; subtract the address in EBX from the address in EAX
  19. ; remember both registers started pointing to the same address (see line 15)
  20. ; but EAX has been incremented one byte for each character in the message string
  21. ; when you subtract one memory address from another of the same type
  22. ; the result is number of segments between them - in this case the number of bytes
  23. mov edx, eax ; EAX now equals the number of bytes in our string
  24. mov ecx, msg ; the rest of the code should be familiar now
  25. mov ebx, 1
  26. mov eax, 4
  27. int 80h
  28. mov ebx, 0
  29. mov eax, 1
  30. int 80h

最基础的字符串输出

首先复习一下单位概念:
1个字节(byte) = 8位(bit)。
位是内存最小单位,只能由0或1表示。
字节是表示整数、地址、字符的单位。
c语言中的char就占用1个字节

以下是汇编”hello,world”的编写方式:

  1. section .data
  2. msg db "hello,world",0d10
  3. section .text
  4. global _start
  5. _start:
  6. mov rax, 0d1 ; rax传递syscall代码1,表示调用函数sys_write
  7. mov rdi, 1 ; 第一个参数,1表示stdout
  8. mov rsi, msg ; 第二个参数,字符串内存地址,表示指针
  9. mov rdx, 0d12; 第三个参数,12表示字符串字节长度(0d101个字节长度)
  10. syscall ; 执行sys_write函数
  11. mov rax, 0d60; rax传递syscall代码60,表示调用函数sys_exit
  12. mov rdi, 0 ; 第一个参数,0表示no error
  13. syscall ; 执行sys_exit函数

只用mov指令及db和syscall两个伪指令,就能实现打印输出,是不是很酷?

改进1:跳转后打印

如同我们在C语言中定义了一个print()函数,然后在main()函数中调用该函数打印一样,在汇编中,也可以使用不同的label(标签)进行跳转,而不必从上到下按顺序执行。

需要用到得新指令为:

  • cmp:compare(比较)指令,用于操作数之间的减法,但不保存结果,只根据结果设置相关的条件标志位(SF、ZF、CF、OF)。cmp指令后往往跟着条件转移指令,实现根据比较的结果产生不同的程序分支的功能。另外有一个sub指令处理减法,结果将保存在寄存器。
  • jz:jump if zero指令,当标志寄存器中的ZF标志为1就跳转,一般与cmp连用,用以判断两数是否相等。

示例:

  1. section .data
  2. msg db "jzjump,ok",0d10
  3. section .text
  4. global _start
  5. _start:
  6. mov r8, 0d2 ; r8 = 2
  7. cmp r8, 0d2 ; r8 - 2 = 0,此时设置ZF=1
  8. jz otherlable ; 零标志为1,跳转到otherlable标签,有些像执行代码快
  9. otherlable:
  10. mov rax, 0d1
  11. mov rdi, 0d1
  12. mov rsi, msg
  13. mov rdx, 0d10
  14. syscall
  15. mov rax, 0d60 ; rax传递syscall代码60,表示调用函数sys_exit
  16. mov rdi, 0 ; 第一个参数,0表示no error
  17. syscall ; 执行sys_exit函数

改进2:计算字符串长度
sys_write要求我们向它传递一个指向我们要在内存中输出的字符串的指针以及我们要打印的字节长度。但是一旦修改字符串,就必须修改传递给sys_write 的字节长度,否则会出现几种情况:

  • 传递的字节长度少于实际字符串的长度,只打印字节长度的字符串,即只能打印一部分。
  • 传递的字节长度多于实际字符串的长度,会有一些空字节,浪费内存空间。
  • 如何不小心填成其他类型数据,会造成程序出错。

因此需要类似length()一样的方式计算字符串长度,内核没有提供这种函数,只能从指令上考虑,我们可以这样做:
可以知道字符串的指针首地址,知道字符串结束后的下一个地址,两个地址相减,得字符串长度。

需要用到的新指令:
inc:increase,自增指令,只有一个操作数,寄存器操作数中的数据内容加1,例如一个操作数为5,执行inc之后,操作数为6。不是地址加1,是内容加1。
jmp:jump,无条件跳转指令,可转到内存中任何程序段。转移地址可在指令中给出,也可以在寄存器中给出,或在储存器中指出。jz和jnz是有跳转跳转的指令,即只有满足某些条件时执行这些指令才会跳转。
sub:subtract减法指令,结果保存在寄存器操作数中

  1. section .data
  2. msg dward
  3. section .text
  4. global _start
  5. _start:
  6. mov r8, 0d2 ; r8 = 2
  7. inc r8 ; r8 = 3
  8. jmp otherlable ; 零标志为1,跳转到otherlable标签,有些像执行代码快
  9. otherlable:
  10. mov rax, r8
  11. mov rdi, 0d1
  12. mov rsi, msg
  13. mov rdx, 0d10
  14. syscall
  15. mov rax, 0d60 ; rax传递syscall代码60,表示调用函数sys_exit
  16. mov rdi, 0 ; 第一个参数,0表示no error
  17. syscall ; 执行sys_exit函数