使用 C 调用约定的汇编语言函数


作者:高玉涵
时间:2021.09.27 17:15
博客:blog.csdn.net/cg_i
环境:Linux 7e142849497c 5.10.47-linuxkit #1 SMP Sat Jul 3 21:51:47 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

为理想奋斗,为真理献身,即便是一无所获,也值得。
——《1921》

暂定第一篇

​ 近段时间一直在参与将应用程序从HP-UX平台移植到Linux的工作。因此,硬件依赖代码、编译环境、调试跟踪产生了浓厚的兴趣,难得有机会窥探代码在不同平台的表现,也借此机会温习蒙尘很久,几近模糊的知识,如,汇编语言。在此期间将所学心得,逐一整理并发布。一来总结巩固知识;二来方便有和同有兴趣的同学共勉;当然,更渴望得到指教。望,此次学习苦旅,能有所收获,亦老怀甚慰了…

环境

​ 限于当前阶段,为使示例清晰,能明确问题。本篇代码指令为 IA-32 格式,采用 GNU 汇编器及 AT&T 操作码语法。

一、使用 C 调用约定的汇编语言函数

​ 汇编语言在函数中处理输入值和输出值可用的选择很多(寄存器、全局变量、堆栈)。虽然这看上去像是好事,但实际上这可能造成问题。如果为大型项目编写函数,确保正确使用每个函数所需的文档就是无法计数的。试图跟踪哪个函数使用哪些寄存器和全局变量,或者使用哪些寄存器和全局变量传递哪些参数,会是程序员的恶梦。

​ 为了帮助解决这个问题,必须使用某一标准一致地存放输入参数以便函数获取,并且一致地存放输出值便于主程序获取。为 IA-32 平台创建代码时,在从 C 函数编译出来的汇编语言代码中,大多数 C 编译器使用标准方法处理输入和输出值。这种方法也适用于任何汇编语言程序,即使它们不是来源于 C 程序。

​ C 把输入值传递给函数的解决方案是使用堆栈。主程序可以访问堆栈,程序中使用的任何函数也可以。这样就创建了在通用的位置在主程序和函数之间传递数据的明确途径,而无需担心破坏寄存器或者定义全局变量。

​ 同样,C 样式定义了把值返回主程序的通用方法——EAX寄存器用于 32 位结果(比如短整数),EDX:EAX 寄存器对于 64 位整数值,FPU 的 ST(0) 寄存器用于浮点值。

1.1 堆栈

​ 我们可以将栈设想为办公桌上的一堆文件,它可以无限增加。我们通常会把工作时间用到的文件放在顶部,而当用完后就把它拿走。计算机的栈处于内存地址的最顶端。你可以通过 pushl 指令将值压入栈顶,该指令将一个寄存器值或内存值压入栈顶。好了,我们说顶,但栈的“顶”实际上是栈内存的底部。这点会令人困惑,因为对于想到的任何一堆东西——盘子、纸等——我们都会想要从顶部添加和删除。然而,在内存中,由于考虑到内存结构,栈是从内存顶部开始向下增长的。因此,当我们提到栈顶,请记住这是在栈内存的底部。你也可以用指令 popl 将值从栈顶弹出。该指令将值从栈顶移除,并把其放入寄存器或你选择的存储位置。

​ 当我们将值入栈时,栈顶会移动,以容纳新增加的值。实际上,我们能不断将值入栈,栈会在内存中保持向下增长,直到达到存放代码或数据的地方。那么,我们如何知道栈当前的顶部在哪里呢?栈寄存器 ESP 总是包含一个指向当前栈顶的指针,无论栈顶在何处。

​ 每当我们用 pushl 将数据入栈,ESP 所含的指针值就会减去 4,从而指向新的栈顶。(记住,每个字的长度为 4 字节,并且栈是向下生长的。)如果我们想从栈中删除数据,只需使用 popl 指令,该指令使 ESP 的值增加 4,并将先前栈顶的值放入你指定的寄存器。 pushl 和 popl 都有一个操作数:对于 pushl,是要将其值入栈的寄存器;对于 popl,是要接收弹出栈数据的寄存器。

1.2 在堆栈之中传递函数参数

​ 在 C 调用约定中,栈是实现函数的局部变量、参数和返回地址的关键因素。

​ 在调用函数之前,主程序把函数所需的参数按逆序压栈中。接着,程序发送一条 call 指令,表明程序希望开始执行哪一个函数。 call 指令会做两件事情:首先,它将下一条指令地址即返回地址压入栈中;然后,将指令指针( EIP )以指向函数开始处。因此,在函数开始执行时,栈看起来如下所示(本例中,栈的”顶部“在底部):

参数 #N
...
参数 2
参数 1
返回地址 <--- ( esp )

​ 堆栈指针( ESP )指向堆栈的顶部,这里存放了返回地址。在堆栈中,函数的所有输入参数都位于返回地址的”下面“(别被上图给蒙蔽了)。把值弹出堆栈以获得输入参数会导致一个问题,因为返回地址可能在处理过程中丢失。替换的做法是,使用其它的方法从堆栈获得输入参数。

​ 因为 ESP 指针指向堆栈的顶部,函数可以根据 ESP 寄存器值的偏移量使用间接寻址访问每个参数,不必把值弹出。

			参数 #N		
			...			间接寻址
			参数 2		8(%esp)
			参数 1		4(%esp)
	esp --->返回地址 	(%esp)

​ 但是,这种技术有个问题。因为在函数中,函数处理的某个部分可能包含把数据压入堆栈的操作。如果发生这种情况,就会改变 ESP 堆栈指针的位置,这将丢失用于访问堆栈中参数的间接寻址值。

​ 为了避免这个问题,通用的做法是进入函数时把 ESP 寄存器复制到 EBP 寄存器。这样确保有一个寄存器永远包含指向调用函数时的堆栈顶部的正确指针。函数执行过程中压入栈的任何数据都不会影响 EBP 寄存器的值。为了避免破坏原始的 EBP 寄存器值,如主程序中使用它的话,在复制 ESP 寄存器的值之前,EBP 寄存器的值也被存放在堆栈中。

​ 现在 EBP 寄存器将一直是栈指针在函数开始时的位置,用于访问函数的参数和局部变量。来自程序的第一个输入参数位于间接寻址位置8 (%ebp) 的位置,等等。可以在函数中使用这些值,而无需担心其它值压入堆栈或者从堆栈中删除。EBP 也可以说是对栈帧的常量引用(栈帧包含一个函数中使用的所有栈变量,包括参数、局部变量和返回地址)。

				参数 #N		
				...				间接寻址
				参数 2		    12(%ebp)
				参数 1		 	8(%ebp)
				返回地址 		4(%ebp)
		esp --->旧的EBP值		(%ebp)
1.3 函数开头和结尾

​ 使用 C 函数样式技术编写的所有函数都使用一组标准指令。下面代码片断演示在函数的开头和结尾使用的指令:

function:
	pushl	%ebp
	movl	%esp,	%ebp
	...
	movl	%ebp,	%esp
	popl	%ebp
	ret

​ 函数代码开头的前两条指令把 EBP 的原始值保存到堆栈的顶部,然后把当前 ESP 堆栈指针(已指向刚保存的原始 EBP 值)复制到 EBP 寄存器。

​ 函数处理完之后,函数的最后两条指令获取存储在 EBP 寄存器中的原始 ESP 寄存器值,并恢复 EBP 寄存器的原始值。重新设置 ESP 寄存器的值确保当执行返回主程序时,函数执行期间存放到堆栈中、但是还没有清除的任何数据都会被丢弃(否则,ret 指令就会返回到错误的内在位置,该指令将栈顶的值弹出,并将指令指针寄存器 EIP 设置为该弹出值,将控制权交还给调用它的程序)。

1.4 定义局部函数数据

​ 当程序控制权在函数代码中时,处理过程很可能需要在某个位置存储数据元素。前面讨论过,可以在函数代码中使用寄存器,但是这种方式只提供数据有限的工作区域。也可以使用全局变量来处理数据,但是问题在于这会额外要求主程序为函数提供专门的数据元素。当在函数中为数据元素的存储寻找方便的位置时,堆栈再一次提供了帮助。

​ EBP 寄存器被设置为指向堆栈顶部之后,函数中使用的任何附加的数据都可以存放在堆栈中这个指针之后,这不会影响对输入值的访问。

				参数 #N		
				...				间接寻址
				参数 2		   	12(%ebp)
				参数 1		 	8(%ebp)
				返回地址 		4(%ebp)
		esp --->旧的EBP值		(%ebp)
				局部变量1		-4(%esp)
				局部变量2		-8(%ebp)

​ 在堆栈中定义局部变量之后,可以使用 EBP 寄存器很容易引用它们。假设对于 4 字节的数据值, 可以通过引用 -4(%ebp) 访问第一个局部变量,引用 -8(%ebp) 访问第二个局部变量。

​ 这种设置还有一个残留问题。如果函数把任何数据压入堆栈,ESP 寄存器仍然指向局部变量被存放之前的位置,并且覆盖这些变量。

​ 为了解决这个问题,在函数代码的开始添加了另一行,通过从 ESP 寄存器减去一个值,为局部变量保留一定数量的堆栈空间。

				参数 #N		
				...			    间接寻址
				参数 2		    12(%ebp)
				参数 1		 	8(%ebp)
				返回地址 		4(%ebp)
				旧的EBP值		(%ebp)
				局部变量1		-4(%esp)
		esp --->局部变量2		-8(%ebp)

​ 现在,如果把任何数据压入堆栈,数据会被存放在局部变量下面,这保护了它们,使得仍然可以通过 EBP 寄存器指针访问它们。一般的 ESP 寄存器仍然可以用于把数据压入堆栈和弹出堆栈,且不会影响局部变量。

​ 到达函数的结尾并且 ESP 寄存器被设置回其原始值时, 局部变量会从堆栈中丢失,并且无法从发出调用的程序使用 ESP 或者 EBP 寄存器直接访问它们(这就是“局部变量”这个术语的由来)。

​ 现在函数开头代码必须包含附加的一行,它通过向下移动堆栈指针,为局部变量保留空间。必须记住为函数中所需的所有局部变量保留足够空间。新的开头就像下面这样:

function:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8,	%esp

​ 这些代码保留 8 字节供局部变量使用。这些空间可以用作 4 个字什,或者 2 个双字值。

1.5 清空堆栈

​ 当使用 C 样式的函数调用时,还有一个细节需要考虑。调用函数之前,发出调用的函数把所有必须的输入值存放在堆栈中。函数返回时,这些值仍然在堆栈中(因为函数访问它们且不把它们弹出堆栈)。如果主程序使用堆栈进行其它操作,它很可能希望从堆栈中删除旧的输入值,以便使堆栈恢复到函数调用之前的状态。

​ 虽然可以使用 popl 指令完成这个工作,但是也可以把 ESP 堆栈指针移动回函数调用之前的原始位置。便用 addl 指令把压入堆栈的数据元素的长度加上去,就完成了这个工作。

​ 例如,如果把两个 4 字节的整数值存放在堆栈中,然后调用函数,那么必须使 ESP 寄存器加上 8 以便把数据清除出堆栈:

pushl %eax
pushl	%ebx
call	function
add	$8,	%esp

​ 这样确保堆栈恢复到应该的状态,以便主程序的其余部份使用。

二、递归函数示例

2.1 阶乘定义

f a c t o r i a l ( n ) = { n ≤ 0 : 1 n > 0 : n ∗ f a c t o r i a l ( n − 1 ) factorial(n) = \begin{cases} n \leq 0:1 \\ n > 0:n * factorial(n -1)\end{cases} factorial(n)={n0:1n>0:nfactorial(n1)
​ 有了上面知识的铺垫想必大家已经了解了如何使用堆栈把输入值传递给函数了,以及如何使用堆栈存储函数中的局部变量,现在是研究例子的时候了。factorial.s 程序将计算一个数的阶乘。阶乘是某个数字与1之间的所有整数的乘积。例如,阶乘 7 是 7 * 6 * 5 * 4 *3 * 2 * 1,4 的阶乘是 4 * 3 * 2 * 1 。现在,你可能发现一个数的阶乘就是该数与比它小 1 的数的阶乘相乘。例如,4 的阶乘是 4 乘以 3 的阶乘,3 的阶乘是 3 乘以 2 的阶乘, 2 的阶乘是 2 乘以 1 的阶乘, 1 的阶乘为 1 。这种类型的定义称为递归定义。这意味着阶乘函数的定义中包括阶乘函数本身。然而,由于所有函数都必须结束,一个递归定义必须包括一个基线条件。基线条件就是递归停止的地方。如果没有基线条件,该函数将不断调用自身,直到最终用尽栈空间。在这个阶乘的示例中,基线条件是数字 1 。当我们偶到数字 1 。就不再调用阶乘;刚才说过阶乘 1 的值为 1 。所以,让我们再看一下希望阶乘函数代码是什么样折。

​ (1) 检查数字
​ (2) 数字是否为 1?
​ (3) 如果数字为 1,答案也为 1。
​ (4) 否则,数字与该数字减去 1 的阶乘相乘。

​ 如果没有局部变量,这样做可能会出问题。在其它程序中,用全局变量存储值动作良好。然而,全局变量只为每个变量提供一个副本。而在这个程序中,在同一时间我们将有函数的多个副本在运行,每个副本都需要自己的数据副本!由于局部变量存在于栈上,每个函数调用都有自己的栈帧,所以我们不会出问题。

​ 代码:

# 目的:	给定一个数字,本程序将计算其阶乘
#		例如,3的阶乘是3x2x1,即6
#		4的阶乘是4x3x2x1,即24,以此类推
#
# 本程序展示了如何递归调用一个函数
.section	.data # 本程序无全局数据
.section	.text

.globl	_start
.globl	factorial		# 除非我们希望与其他程序共享该函数,否则无需此项

_start:
	pushl	$4			# 阶乘有一个参数,就是我想要为其计算阶乘的数字.
						# 因此,该数字入栈
	call	factorial	# 运行阶乘函数
					    # 并将下条指定地址保存至栈中,并将eip指向函数
						
	addl	$4,	%esp		# 返回到$4入栈前的位置,弹出入栈的参数,清空栈
	movl	%eax,	%ebx	# 阶乘将答案返回到%eax
							# 但我们希望它在%ebx中,
							# 这样可将之作为我们的退出状态
							
	movl	$1,	%eax		# 调用内核退出函数
	int	$0x80
	
	# 这是实际的函数定义
	.type	factorial,@function
	factorial:
		pushl	%ebp	# 保存原ebp内容.标准函数 - 我们必须在返回前
						# 恢复ebp到其之前的状态,因此我们必须将其入栈.
							
		movl	%esp,	%ebp # 这条指令是因为我们不想更改
							 # 栈指针,所以使用ebp
							 # 用作索引访问局部变量
		movl	8(%ebp), %eax # 这条指令将第一个参数移入eax
						      # 4(%ebp)保存返回地址,而
							  # 8(%ebp)保存第一个参数
		cmpl	$1,	%eax	# 如果数字为1,这就是我们的基线条件
						    # 我们只要返回即可(1已经作为返回值在eax中)
		je end_factorial
		
		decl	%eax		# 否则,递减值
		pushl	%eax		# 为了调用factorial函数将其入栈
		call	factorial	# 调用factorial函数
		movl	8(%ebp), %ebx	 # eax中为返回值,因此我们将参数
								 # 重新加载至ebx
		imull	%ebx,	%eax     # 将之与上一次调用
								 # factorial的结果(在eax中)相乘
								 # 答案将存入eax,而eax正是
								 # 存放返回值的地方
		end_factorial:
			movl %ebp,	%esp	 # 标准函数返回相关处理 - 我们
			popl %ebp			 # 必须将ebp和esp恢复到
								 # 函数开始运行前的状态
			ret				     # 返回到函数(这也会将返回值弹出栈)
2.2 编译程序生成目标文件

​ 将上面源代码文件转换为机器可读的形式,我们需要汇编链接程序。在下面的命令中,as 是运行汇编程序的命令( --gstabs 后面会讲),factorial.s 是源文件,-o factorial.o 告诉汇编程序将输出放在文件 factorial.o 中,factorial.o 称为目标文件。在我的机器上运行的是 Ubuntu x86_64 位的操作系统,编译器默认采用的是 x86_64 指令,–32 将以 IA_32 指令输出目标文件。目标文件是用机器语言写成的代码。目标文件的内容通常不完全放在一处。许多大型程序有多个源文件,通常将每个源文件都转换成一个目标文件。这时,链接器程序将把多个目标文件合而为一,并向其中添加信息,以使内核知道如何加载和运行该目标文件。在本例中,我们只有一个目标文件,所以链接器只需要添加信息使之运行即可。链接 factorial.o 文件的命令是 ld ,factorial.o 是我要链接的目标文件,-o factorial 指示链接器输出新程序到名为 factorial 的文件。-m elf_i386 以ELF32格式输出。图1-1演示这一过程。

   as --gstabs factorial.s -o factorial.o --32
   ld -m elf_i386 factorial.o -o factorial
源代码文件
编译器
目标代码文件
链接器
可执行文件
其它目标代码文件
目标代码库
图1-1
2.3 readlf -e 查看 ELF 信息
root@eca0d45596c0:~/html# readelf -e factorial
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048054
  Start of program headers:          52 (bytes into file)
  Start of section headers:          448 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         1
  Size of section headers:           40 (bytes)
  Number of section headers:         7
  Section header string table index: 4

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        08048054 000054 00002f 00  AX  0   0  1
  [ 2] .stab             PROGBITS        00000000 000084 0000fc 0c      3   0  4
  [ 3] .stabstr          STRTAB          00000000 000180 00000d 00      0   0  1
  [ 4] .shstrtab         STRTAB          00000000 00018d 000030 00      0   0  1
  [ 5] .symtab           SYMTAB          00000000 0002d8 0000c0 10      6   7  4
  [ 6] .strtab           STRTAB          00000000 000398 000044 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x08048000 0x08048000 0x00083 0x00083 R E 0x1000

 Section to Segment mapping:
  Segment Sections...
   00     .text 	
2.4 使用 objdump

​ objdump 程序是对程序员非常有用的工具。objdump 不仅能够显示汇编语言代码,而且能够显示生成的原始指令码(详细用法大家可自行搜索)。

root@7e142849497c:~/html# objdump -d factorial

factorial:     file format elf32-i386

Disassembly of section .text:

08048054 <_start>:
 8048054:	6a 04                push   $0x4
 8048056:	e8 0c 00 00 00       call   8048067 <factorial>
 804805b:	83 c4 04             add    $0x4,%esp
 804805e:	89 c3                mov    %eax,%ebx
 8048060:	b8 01 00 00 00       mov    $0x1,%eax
 8048065:	cd 80                int    $0x80

08048067 <factorial>:
 8048067:	55                   push   %ebp
 8048068:	89 e5                mov    %esp,%ebp
 804806a:	8b 45 08             mov    0x8(%ebp),%eax
 804806d:	83 f8 01             cmp    $0x1,%eax
 8048070:	74 0d                je     804807f <end_factorial>
 8048072:	48                   dec    %eax
 8048073:	50                   push   %eax
 8048074:	e8 ee ff ff ff       call   8048067 <factorial>
 8048079:	8b 5d 08             mov    0x8(%ebp),%ebx
 804807c:	0f af c3             imul   %ebx,%eax

0804807f <end_factorial>:
 804807f:	89 ec                mov    %ebp,%esp
 8048081:	5d                   pop    %ebp
 8048082:	c3                   ret    
2.5 GDB调试器程序

​ gdb常常是Linux和BSD开发系统的标准组件。要使用调试器,就必须使用–gstabs选项编译或者汇编可执行文件,这样使可执行文件内包含必须的信息,以便调试器知道指令码与源代码文件中什么位置是相关联的。gdb启动之后,它使用一个命令界面接收调试命令:

root@eca0d45596c0:~/html# gdb factorial
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.3) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from factorial...done.
(gdb) 

​ gdb命令提示下,可以输入调试命令。可以使用的命令的清单很长。下列出了本次需要用到的命令。

命令描述示例说明
break在源代码中设置断点以便停止执行b _start程序开始处
info观察系统元素,比如寄存器、堆栈和内存i r查看所有寄存器
x观察内在位置x /x $esp以16进制值显示esp寄存器值
print显示变量值print /d $eax以10进制值显示eax寄存器值
run在调试器内开始程序的执行run程序开始执行
step执行程序中的下一条指令s执行下一条指令
2.6 在操作之中监视堆栈

​ 通过使用调试器,可以监视程序运行时所有数据值是如何存放到堆栈区域中的。在调试器中运行程序,在程序的开始设置断点,并且查看堆栈指针设置在什么位置:

root@7e142849497c:~/html# gdb factorial
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from factorial...
(gdb) b _start
Breakpoint 1 at 0x8048054: file factorial.s, line 16.
(gdb) run
Starting program: /var/www/html/factorial 
warning: Error disabling address space randomization: 不允许的操作

Breakpoint 1, _start () at factorial.s:16
16		pushl	$4			# 阶乘有一个参数,就是我想要为其计算阶乘的数字.
(gdb) i r
eax            0x0                 0
ecx            0x0                 0
edx            0x0                 0
ebx            0x0                 0
esp            0xff97ca30          0xff97ca30
ebp            0x0                 0x0
esi            0x0                 0
edi            0x0                 0
eip            0x8048054           0x8048054 <_start>
eflags         0x202               [ IF ]
cs             0x23                35
ss             0x2b                43
ds             0x2b                43
es             0x2b                43
fs             0x0                 0
gs             0x0                 0
(gdb) s
18		call	factorial	# 运行阶乘函数
(gdb) x /x $esp
0xff97ca2c:	0x00000004
(gdb) x /x $eip
0x8048056 <_start+2>:	0x00000ce8

三、 函数调试跟踪表

序号ESP指针说明EIP指针指令EBPEAXEBXEFLAGS
0xff97ca30执行前0x8048054pushl $4000IF
160xff97ca2c4参数0x8048056call factorial
180xff97ca280x0804805b返回0x8048067pushl %ebp
320xff97ca240原EBP0x8048068movl %esp,%ebp
350x804806amovl 8(%ebp),%eax0xff97ca24
380x804806dcmpl $1,%eax4
410x8048070je end_factorialPF IF
430x8048072decl %eax
450x8048073pushl %eax3
460xff97ca203参数0x8048074call factorial
470xff97ca1c0x08048079返回0x8048067pushl %ebp
320xff97ca180xff97ca24原EBP0x8048068movl %esp,%ebp
350x804806amovl 8(%ebp),%eax0xff97ca18
380x804806dcmpl $1,%eax3
410x8048070je end_factorialIF
430x8048072decl %eax
450x8048073pushl %eax2
460xff97ca142参数0x8048074call factorial
470xff97ca100x08048079返回0x8048067pushl %ebp
320xff97ca0c0xff97ca18原EBP0x8048068movl %esp,%ebp
350x804806amovl 8(%ebp),%eax0xff97ca0c
380x804806dcmpl $1,%eax2
410x8048070je end_factorial
430x8048072decl %eax
450x8048073pushl %eax1
460xff97ca081参数0x8048074call factorial
470xff97ca040x08048079返回0x8048067pushl %ebp
320xff97ca000xff97ca0c原EBP0x8048068movl %esp,%ebp
350x804806amovl 8(%ebp),%eax0xff97ca00
380x804806dcmpl $1,%eax1
410x8048070je end_factorialPF ZF IF
430x804807fmovl %ebp,%esp
550xff97ca000xff97ca0c原ESP0x8048081popl %ebp
560xff97ca040x08048079返回0x8048082ret0xff97ca0c
580xff97ca081参数0x8048079movl 8(%ebp),%ebx
480x804807cimull %ebx,%eax2
500x804807fmovl %ebp,%esp2
550xff97ca0c0xff97ca18原ESP0x8048081popl %ebp
560xff97ca100x08048079返回0x8048082ret0xff97ca18
580xff97ca142参数0x8048079movl 8(%ebp),%ebx
480x804807cimull %ebx,%eax3
500x804807fmovl %ebp,%esp6
550xff97ca180xff97ca24原ESP0x8048081popl %ebp
560xff97ca1c0x08048079返回0x8048082ret
580xff97ca203参数0x8048079movl 8(%ebp),%ebx0xff97ca24
480x804807cimull %ebx,%eax4
500x804807fmovl %ebp,%esp24
550xff97ca240原ESP0x8048081popl %ebp
560xff97ca280x0804805b返回0x8048082ret0
580xff97ca2c4参数0x804805baddl $4,%esp
210xff97ca30执行前0x804805emovl %eax,%ebx
220x8048060movl $1,%eax24
260x8048065int $0x801
27结束

​ 表格中的空项,表示这期间执行的代码没有更改过数据。如,ESP 指针列,第 32 行起到 45 行,值都为0xff97ca24,直到执行到第46行,值变更为 0xff97ca14 。EIP 指针永远指向下一条需要执行的代码位置。如,第 16 行 EIP 值为 0x8048056 call factorial 指令执行后的效果在第 18 行的表格反应出来。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值