X86初学者汇编程序指南

如今,几乎不需要用纯汇编程序编写代码,但是我绝对推荐给对编程感兴趣的任何人。 您会从不同的角度来看事情,并且在调试其他语言的代码时,技能会派上用场。

在本文中,我们将从头开始编写纯x86汇编器中的反向波兰表示法(RPN)计算器。 完成后,我们可以像这样使用它:

$ ./calc "32+6*" # "(3+2)*6"    30 

本文的所有代码都在这里 。 它被大量注释掉,并且可以作为已经知道汇编程序的人的教材。

让我们开始编写基本的Hello world程序! 检查环境设置。 然后,继续进行系统调用,调用堆栈,堆栈帧和x86调用约定。 然后,为了进行练习,我们将在x86汇编器中编写一些基本功能-并开始编写RPN计算器。

假定读者具有一定的C语言编程经验和计算机体系结构的基本知识(例如,什么是处理器寄存器)。 由于我们将使用Linux,因此您还应该能够使用Linux命令行。

环境设定


如前所述,我们使用Linux(64位或32位)。 上面的代码在Windows或Mac OS X上不起作用。

对于安装,您仅需要binutils的GNU ld链接器(它已预先安装在大多数发行版中)和NASM汇编器中。 在Ubuntu和Debian上,您可以使用一个命令来安装这两者:

 $ sudo apt-get install binutils nasm 

我还建议您方便使用ASCII表

世界,您好!


要验证环境,请将以下代码保存在calc.asm文件中:

 ;    _start     ; . global _start ;   .rodata   (  ) ;     ,       section .rodata ;     hello_world.   NASM ;   ,     , ;  . 0xA =  , 0x0 =    hello_world: db "Hello world!", 0xA, 0x0 ;   .text,     section .text _start: mov eax, 0x04 ;   4   eax (0x04 = write()) mov ebx, 0x1 ;   (1 =  , 2 =  ) mov ecx, hello_world ;     mov edx, 14 ;   int 0x80 ;    0x80,   ;     mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 =   int 0x80 

注释解释了一般结构。 有关寄存器列表和一般说明,请参见《 弗吉尼亚大学x86汇编程序指南》 。 随着对系统调用的进一步讨论,这将变得更加必要。

以下命令将汇编程序文件收集到目标文件中,然后编译可执行文件:

 $ nasm -f elf_i386 calc.asm -o calc $ ld -m elf_i386 calc.o -o calc 

启动后,您应该看到:

 $ ./calc Hello world! 

生成文件


这是一个可选部分,但是您可以制作一个Makefile来简化将来的构建和布局。 将其保存在与calc.asm相同的目录中:

 CFLAGS= -f elf32 LFLAGS= -m elf_i386 all: calc calc: calc.o ld $(LFLAGS) calc.o -o calc calc.o: calc.asm nasm $(CFLAGS) calc.asm -o calc.o clean: rm -f calc.o calc .INTERMEDIATE: calc.o 

然后,代替上面的说明,只需运行make。

系统调用


Linux系统调用告诉OS为我们做些事情。 在本文中,我们仅使用两个系统调用: write()将行写入文件或流(在我们的示例中,这是标准输出设备和标准错误),而exit()退出程序:

 syscall 0x01: exit(int error_code) error_code -  0         (  1)   syscall 0x04: write(int fd, char *string, int length) fd —  1   , 2      string —      length 

通过将系统调用号存储在eax寄存器中,然后将其参数依次存储在ebxecxedx中,来配置系统调用。 您可能会注意到exit()exit()一个参数-在这种情况下ecx和edx无关紧要。

ax埃克斯ecxedx
系统电话号码arg1arg2arg3


调用堆栈




调用堆栈是一种数据结构,用于存储有关对函数的每次调用的信息。 每个调用在堆栈中都有其自己的部分-“框架”。 它存储有关当前调用的一些信息:此函数的局部变量和返回地址(函数执行后程序应到达的位置)。

我立即注意到一个不明显的事情:堆栈减少了内存。 当您将某些内容添加到堆栈的顶部时,它会插入到比上一项低的内存地址中。 换句话说,随着堆栈的增长,堆栈顶部的内存地址会减少。 为避免混淆,我将始终提醒您这个事实。

push指令将某些东西push堆栈顶部,然后pop从那里弹出数据。 例如, push 在堆栈的顶部分配一个位置,并将eax寄存器中的值放在此处,而pop 将所有数据从堆栈的顶部传送到eax并释放此内存区域。

esp寄存器的目的是指向堆栈的顶部。 超过esp任何数据都esp认为不会到达堆栈,这是垃圾数据。 执行push (或pop )语句会移动esp 。 如果对操作进行报告,则可以直接操作esp

ebp寄存器与esp相似,只是它总是始终指向当前堆栈帧的中间,紧接当前函数的局部变量之前(我们将在后面讨论)。 但是,调用另一个函数不会自动移动ebp ,必须每次手动完成。

X86体系结构调用约定


在x86中,没有像高级语言中的内置函数概念。 call goto基本上只是到另一个内存地址的jmpgoto )。 要将例程用作其他语言的函数(可以接受参数并返回数据),则需要遵循调用约定(有很多约定,但我们使用CDECL,这是C编译器和汇编程序员中x86最受欢迎的约定)。 它还可以确保在调用另一个函数时不会混淆常规寄存器。

来电者规则


在调用函数之前,调用者必须:

  1. 将调用者需要保存的寄存器保存到堆栈中。 被调用的函数可以更改某些寄存器:为了不丢失数据,调用者必须将其保存在内存中,直到将其压入堆栈为止。 这些是eaxecxedx 。 如果您不使用它们中的任何一个,那么您将无法保存它们。
  2. 以相反的顺序将函数参数写入堆栈(第一个最后一个参数,最后一个第一个参数)。 此顺序可确保被调用函数以正确的顺序从堆栈接收其参数。
  3. 调用子程序。

如果可能,该函数会将结果保存在eaxcall呼叫者应立即:

  1. 从堆栈中删除函数参数。 这通常是通过简单地将字节数添加到esp 。 不要忘了堆栈会变小,因此要从堆栈中删除,必须添加字节。
  2. 通过以相反的顺序从堆栈弹出保存的寄存器来恢复它们。 被调用的函数不会更改任何其他寄存器。

下面的示例演示如何应用这些规则。 假定_subtract函数采用两个整数(4字节)参数,并返回第一个参数减去第二个参数。 在_mysubroutine子例程中_mysubroutine使用参数102调用_subtract

 _mysubroutine: ; ... ;  -  ; ... push ecx ;   (    eax) push edx push 2 ;  ,      push 10 call _subtract ; eax   10-2=8 add esp, 8 ;  8    (   4 ) pop edx ;    pop ecx ; ... ;  - ,        eax ; ... 

被调用程序的规则


在调用之前,子例程必须:

  1. 通过将前一帧的ebp基址寄存器指针写入堆栈来保存它。
  2. 将前一帧的ebp调整为当前(当前esp值)。
  3. 在堆栈上为局部变量分配更多空间,如有必要,请移动esp指针。 随着堆栈的减少,您需要从esp减去丢失的内存。
  4. 将被调用例程的寄存器保存到堆栈中。 这些是ebxediesi 。 不必保存未计划更改的寄存器。

步骤1之后调用堆栈:



步骤2之后的呼叫堆栈:



在第4步之后调用堆栈:



在这些图中,在每个堆栈帧中都指示了返回地址。 它通过call语句自动推入堆栈。 ret从堆栈顶部检索地址并跳转到该地址。 我们不需要此指令,我只是说明了为什么函数的局部变量比ebp 4个字节,而函数的参数比ebp低8个字节。

在最后一张图中,您还可以注意到该函数的局部变量总是从ebp-4地址的ebp-4上方开始4个字节(此处减去,因为我们正在向上移动堆栈),并且该函数的参数总是从ebp+8地址的ebp+8下方8个字节开始ebp+8 (此外,因为我们要向下移动堆栈)。 如果您遵循此约定的规则,那么任何函数的变量和参数都将遵循这种约定。

函数完成并要返回时,如果需要,必须首先将eax设置为函数的返回值。 此外,您还需要:

  1. 通过以相反的顺序从堆栈弹出保存的寄存器来恢复它们。
  2. 如有必要,释放由局部变量在步骤3中分配的堆栈上的空间:只需在esp中安装esp
  3. 通过从堆栈中弹出来恢复前一帧的ebp基本指针。
  4. 退货退货

现在我们从示例中实现_subtract函数:

 _subtract: push ebp ;      mov ebp, esp ;  ebp ;          ,      ;       ,     ;   ;    mov eax, [ebp+8] ;      eax.  ;       ebp+8 sub eax, [ebp+12] ;      ebp+12   ;  ;   , eax     ;     ,     ;       ,       pop ebp ;      ret 

出入境


在上面的示例中,您会注意到该函数始终以相同的方式运行: push ebpmov ebpesp和局部变量的内存分配。 x86集具有执行所有操作的便捷指令: enter ab ,其中a是要分配给局部变量的字节数, b是“嵌套级别”,我们将始终将其设置为0 。 此外,该函数始终以pop ebpmov espebp指令结尾(尽管它们仅在为局部变量分配内存时才是必需的,但在任何情况下都没有害处)。 也可以用一个语句代替: leave 。 我们进行更改:

 _subtract: enter 0, 0 ;        ebp ;       ,     ;   ;    mov eax, [ebp+8] ;      eax.  ;       ebp+8 sub eax, [ebp+12] ;      ebp+12  ;   ;   , eax     ;     ,     leave ;      ret 

编写一些基本功能


掌握了调用约定后,就可以开始编写一些例程了。 为什么不对显示“ Hello world!”的代码进行泛化呢?要输出任何行: _print_msg函数。

在这里,我们需要另一个_strlen函数来计算字符串的长度。 在C中,它可能看起来像这样:

 size_t strlen(char *s) { size_t length = 0; while (*s != 0) { //   length++; s++; } //   return length; } 

换句话说,从该行的最开始,我们为除零之外的每个字符的返回值加1。 一旦注意到空字符,我们将返回循环中累积的值。 在汇编程序中,这也非常简单:您可以使用先前编写的_subtract函数作为基础:

 _strlen: enter 0, 0 ;        ebp ;       ,     ;   ;    mov eax, 0 ; length = 0 mov ecx, [ebp+8] ;    (   ;  )   ecx (   ; ,      ) _strlen_loop_start: ;  ,    cmp byte [ecx], 0 ;       .  ;     32  (4 ). ;    .    ;     ( ) je _strlen_loop_end ;       inc eax ;    ,  1    add ecx, 1 ;       jmp _strlen_loop_start ;      _strlen_loop_end: ;   , eax    ;     ,     leave ;      ret 

已经不错了吧? 首先编写C代码可能会有帮助,因为大多数代码都直接转换为汇编程序。 现在,您可以在_print_msg使用此函数,我们将在其中应用所获得的所有知识:

 _print_msg: enter 0, 0 ;    mov eax, 0x04 ; 0x04 =   write() mov ebx, 0x1 ; 0x1 =   mov ecx, [ebp+8] ;       , ;   edx   .    _strlen push eax ;     (    edx) push ecx push dword [ebp+8] ;   _strlen  _print_msg.  NASM ; ,    ,  , . ;      dword (4 , 32 ) call _strlen ; eax     mov edx, eax ;     edx,     add esp, 4 ;  4    ( 4-  char*) pop ecx ;     pop eax ;      _strlen,     int 0x80 leave ret 

在完整的程序“ Hello,world!”中使用此功能,可以看到我们辛勤工作的成果。

 _start: enter 0, 0 ;     (    ) push hello_world ;    _print_msg call _print_msg mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 =   int 0x80 

信不信由你,我们涵盖了编写基本x86汇编程序所需的所有主要主题! 现在我们已经有了所有的入门材料和理论,因此我们将完全专注于代码并应用所学到的知识来编写我们的RPN计算器。 函数将更长,甚至会使用一些局部变量。 如果您想立即查看完成的程序, 则为

对于不熟悉反向波兰表示法(有时称为反向波兰表示法或后缀表示法)的人,此处的表达式是使用堆栈求值的。 因此,您需要创建一个堆栈以及_pop_push来操作该堆栈。 您还将_print_answer函数,该函数将在计算结束时输出数字结果的字符串表示形式。

堆栈创建


首先,我们为堆栈定义内存空间,以及全局变量stack_size 。 建议更改这些变量,以使它们不属于.rodata部分,而属于.data

 section .data stack_size: dd 0 ;   dword (4 )   0 stack: times 256 dd 0 ;    

现在,您可以实现_push_pop

 _push: enter 0, 0 ;    ,    push eax push edx mov eax, [stack_size] mov edx, [ebp+8] mov [stack + 4*eax], edx ;    .   ;       dword inc dword [stack_size] ;  1  stack_size ;     pop edx pop eax leave ret _pop: enter 0, 0 ;     dec dword [stack_size] ;   1  stack_size mov eax, [stack_size] mov eax, [stack + 4*eax] ;       eax ;     ,     leave ret 

号码输出


_print_answer更为复杂:您必须将数字转换为字符串并使用其他几个功能。 您将_putc函数(输出一个字符), mod函数(用于计算两个参数的除法(模块)的余数)和_pow_10以10的幂为单位)。 这很简单,下面是代码:

 _pow_10: enter 0, 0 mov ecx, [ebp+8] ;  ecx (  )  ;  mov eax, 1 ;   10 (10**0 = 1) _pow_10_loop_start: ;  eax  10,  ecx   0 cmp ecx, 0 je _pow_10_loop_end imul eax, 10 sub ecx, 1 jmp _pow_10_loop_start _pow_10_loop_end: leave ret _mod: enter 0, 0 push ebx mov edx, 0 ;   mov eax, [ebp+8] mov ebx, [ebp+12] idiv ebx ;  64-  [edx:eax]  ebx.    ;  32-  eax,    edx  ; . ;    eax,   edx.  ,  ;       , ;    . mov eax, edx ;     () pop ebx leave ret _putc: enter 0, 0 mov eax, 0x04 ; write() mov ebx, 1 ;   lea ecx, [ebp+8] ;   mov edx, 1 ;   1  int 0x80 leave ret 

那么,我们如何得出数字中的单个数字? 首先,请注意,数字的最后一位是除以10的余数(例如123 % 10 = 3 ),下一位是除以100并除以10的余数(例如(123 % 100)/10 = 2 )。 通常,您可以通过找到( % 10**n) / 10**(n-1)来找到数字的特定位数(从右到左),其中单位数为n = 1 ,十位数为n = 2依此类推。

使用此知识,您可以找到数字的所有位,从n = 1n = 10 (这是有符号4字节整数中的最大位数)。 但是,从左到右移动要容易得多-因此我们可以在找到每个字符后立即打印每个字符,并消除左侧的零。 因此,我们对n = 10n = 1的数字进行排序。

在C语言中,程序将如下所示:

 #define MAX_DIGITS 10 void print_answer(int a) { if (a < 0) { //    putc('-'); //   «» a = -a; //     } int started = 0; for (int i = MAX_DIGITS; i > 0; i--) { int digit = (a % pow_10(i)) / pow_10(i-1); if (digit == 0 && started == 0) continue; //     started = 1; putc(digit + '0'); } } 

现在您了解了我们为什么需要这三个功能。让我们在汇编器中实现这一点:

 %define MAX_DIGITS 10 _print_answer: enter 1, 0 ;  1    "started"   C push ebx push edi push esi mov eax, [ebp+8] ;   "a" cmp eax, 0 ;    ,    ;  jge _print_answer_negate_end ; call putc for '-' push eax push 0x2d ;  '-' call _putc add esp, 4 pop eax neg eax ;     _print_answer_negate_end: mov byte [ebp-4], 0 ; started = 0 mov ecx, MAX_DIGITS ;  i _print_answer_loop_start: cmp ecx, 0 je _print_answer_loop_end ;  pow_10  ecx.   ebx   "digit"   C. ;    edx = pow_10(i-1),  ebx = pow_10(i) push eax push ecx dec ecx ; i-1 push ecx ;    _pow_10 call _pow_10 mov edx, eax ; edx = pow_10(i-1) add esp, 4 pop ecx ;   i  ecx pop eax ; end pow_10 call mov ebx, edx ; digit = ebx = pow_10(i-1) imul ebx, 10 ; digit = ebx = pow_10(i) ;  _mod  (a % pow_10(i)),   (eax mod ebx) push eax push ecx push edx push ebx ; arg2, ebx = digit = pow_10(i) push eax ; arg1, eax = a call _mod mov ebx, eax ; digit = ebx = a % pow_10(i+1), almost there add esp, 8 pop edx pop ecx pop eax ;   mod ;  ebx ( "digit" )  pow_10(i) (edx).    ; ,   idiv     edx, eax.  ; edx   ,    - ;   push esi mov esi, edx push eax mov eax, ebx mov edx, 0 idiv esi ; eax   () mov ebx, eax ; ebx = (a % pow_10(i)) / pow_10(i-1),  "digit"   C pop eax pop esi ; end division cmp ebx, 0 ;  digit == 0 jne _print_answer_trailing_zeroes_check_end cmp byte [ebp-4], 0 ;  started == 0 jne _print_answer_trailing_zeroes_check_end jmp _print_answer_loop_continue ; continue _print_answer_trailing_zeroes_check_end: mov byte [ebp-4], 1 ; started = 1 add ebx, 0x30 ; digit + '0' ;  putc push eax push ecx push edx push ebx call _putc add esp, 4 pop edx pop ecx pop eax ;   putc _print_answer_loop_continue: sub ecx, 1 jmp _print_answer_loop_start _print_answer_loop_end: pop esi pop edi pop ebx leave ret 

这是一个艰难的考验!希望评论有助于解决。如果您现在认为:“您为什么不能编写printf("%d")?”,那么您将喜欢本文的结尾,在此我们将函数替换为!

现在,我们拥有所有必需的功能,剩下的就是在其中实现基本逻辑了,仅此而已_start

反向波兰语符号计算


如前所述,反向波兰语表示法是使用堆栈进行计算的。读取时,将数字压入堆栈,读取时,将运算符应用于堆栈顶部的两个对象。

例如,如果我们要计算84/3+6*(此表达式也可以以形式编写6384/+*),则过程如下:

步骤记号叠前叠后
1个8[][8]
24[8][8, 4]
3/[8, 4][2]
43[2][2, 3]
5+[2, 3][5]
66[5][5, 6]
7*[5, 6][30]

, — , . 30.

C:

 int stack[256]; // , 256      int stack_size = 0; int main(int argc, char *argv[]) { char *input = argv[0]; size_t input_length = strlen(input); for (int i = 0; i < input_length; i++) { char c = input[i]; if (c >= '0' && c <= '9') { //   —   push(c - '0'); //          } else { int b = pop(); int a = pop(); if (c == '+') { push(a+b); } else if (c == '-') { push(ab); } else if (c == '*') { push(a*b); } else if (c == '/') { push(a/b); } else { error("Invalid input\n"); exit(1); } } } if (stack_size != 1) { error("Invalid input\n"); exit(1); } print_answer(stack[0]); exit(0); } 

, , .

 _start: ;  _start   ,    . ;   esp    argc ( ),  ; esp+4   argv. , esp+4    ; , esp+8 -       mov esi, [esp+8] ; esi = "input" = argv[0] ;  _strlen      push esi call _strlen mov ebx, eax ; ebx = input_length add esp, 4 ; end _strlen call mov ecx, 0 ; ecx = "i" _main_loop_start: cmp ecx, ebx ;  (i >= input_length) jge _main_loop_end mov edx, 0 mov dl, [esi + ecx] ;          ; edx.   edx . ; edx =  c = input[i] cmp edx, '0' jl _check_operator cmp edx, '9' jg _print_error sub edx, '0' mov eax, edx ; eax =  c - '0' (,  ) jmp _push_eax_and_continue _check_operator: ;   _pop    b  edi, a  b -  eax push ecx push ebx call _pop mov edi, eax ; edi = b call _pop ; eax = a pop ebx pop ecx ; end call _pop cmp edx, '+' jne _subtract add eax, edi ; eax = a+b jmp _push_eax_and_continue _subtract: cmp edx, '-' jne _multiply sub eax, edi ; eax = ab jmp _push_eax_and_continue _multiply: cmp edx, '*' jne _divide imul eax, edi ; eax = a*b jmp _push_eax_and_continue _divide: cmp edx, '/' jne _print_error push edx ;  edx,      idiv mov edx, 0 idiv edi ; eax = a/b pop edx ;   eax     _push_eax_and_continue: ;  _push push eax push ecx push edx push eax ;   call _push add esp, 4 pop edx pop ecx pop eax ;  call _push inc ecx jmp _main_loop_start _main_loop_end: cmp byte [stack_size], 1 ;  (stack_size != 1),   jne _print_error mov eax, [stack] push eax call _print_answer ; print a final newline push 0xA call _putc ; exit successfully mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 =   int 0x80 ;    _print_error: push error_msg call _print_msg mov eax, 0x01 mov ebx, 1 int 0x80 

error_msg .rodata :

 section .rodata ;     error_msg.  db  NASM ;    ,     ; . 0xA =  , 0x0 =    error_msg: db "Invalid input", 0xA, 0x0 

! , . , , , , , RollerCoaster Tycoon!

. ! , .


, :

  1. segfault , .
  2. .
  3. .
  4. .
  5. _strlen C , _print_answer printf .


Source: https://habr.com/ru/post/zh-CN423077/


All Articles