CSPP学习笔记-Ch3.7 过程

3.7 过程(Procedures)

过程:软件的一种抽象。
提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能;
可以在程序的不同地方对其进行调用。
形式包括:函数(function)、方法(method)、子例程(subroutine)、处理(响应)函数(handler)等。

过程的机器级支持,需要包含以下机制:假设过程 P 调用过程 Q,Q 执行后返回到 P。
1、传递控制:在进入过程 Q 的时候,程序计数器必须被设置为 Q 的代码的起始地址;返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
2、传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 返回一个值。
3、分配和释放内存:在开始时,Q 需要为局部变量分配空间;返回前,又必须释放这些存储空间。

3.7.1 运行时的栈(The Run-Time Stack)(重要)

过程调用机制的一个关键特性:使用了数据结构提供的后进先出的内存管理原则。

1、分配和释放栈空间

  • 分配空间
    • 使用 pushq 将数据存入栈中;
    • 操作栈指针:减小一个适当的量可以为没有指定初始值的数据在栈上分配空间。
  • 释放空间
    • 使用 popq 将数据弹出栈;
    • 操作栈指针:增加一个适当的量可以释放栈上已分配的空间。
      (注意:操作的是指针,实际上并不会影响到栈空间所映射的内存位置的值)

2、何时会用到栈?栈帧是啥?

(1)函数参数保存在哪里?

3.4 节给出的整数寄存器表格是 x86-64 的一组共计 16 个通用目的寄存器,每个 64 位。
其中有 6 个寄存器可以用于函数传参,按参数出现顺序分别是 %rdi%rsi%rdx%rcx%r8%r9(64位时的名称)。——函数参数就临时保存在这些寄存器里

【问题】函数参数超过 6 个怎么办?有些 CPU 甚至只提供 4 个可用于函数传参的寄存器
【方法】在调用函数传参之前,把寄存器无法存下的参数存储在自己(当前执行的过程)的栈帧中。

【扩展】这也是为什么不建议函数参数过多的原因。多了需要使用栈空间,增加了存取释放的时间,降低了函数调用的速度。

(2)栈帧
image-20211211163031499

过程需要的存储空间超出寄存器能够提供的大小时,就会在栈上分配空间,该空间被称为: 栈帧(stack frame)。

  • 栈帧的边界默认以 16 个字节进行对齐,栈帧内部则按正常规则对齐。(——主要是为了支持 SSE(Streaming SIMD Extensions)浮点运算扩展指令集与16字节的 XMM 浮点寄存器)
  • 栈帧的大小
    • 定长栈帧:编译器在编译时根据一些机制确定一个固定长度的空间;
    • 变长栈帧:有些函数需要的局部存储空间是变长的。
      🌰 当函数调用 alloca 时就会发生这种情况,alloca 是一个标准库函数,可以栈上分配任意字节数量的存储。
      🌰 当代码声明一个局部变长数组时,也会发生这种情况。

举个🌰:

/* 一个包含变长数组的🌰 */
long vframe(long n, long idx, long *q) {
    long i;
    long *p[n];
    p[0] = &i;
    for (i = 1; i < n; i++)
    	p[i] = q;
    return *p[idx];
}
# long vframe(long n, long idx, long *q)
# n in %rdi, idx in %rsi, q in %rdx
# Only portions of code shown
vframe:
	## 把%rbp的当前值压入栈中,再将%rbp设置为指向当前的栈位置。
	pushq %rbp 					# Save old %rbp
	movq %rsp, %rbp 			# Set frame pointer
	## 在栈上分配16个字节,其中前8个字节用于存储局部变量i,后8个字节是编译器机制决要分配的预留空间(这里不用管)。
	subq $16, %rsp 				# Allocate space for i (%rsp = s1,结合下图看)
	## 为数组p分配空间,怎么分配的详看后续单独分析。
	leaq 22(,%rdi,8), %rax
	andq $-16, %rax
	subq %rax, %rsp 			# Allocate space for array p(%rsp = s2,结合下图看)
	## 找到数组p的初始地址
	leaq 7(%rsp), %rax
	shrq $3, %rax
	leaq 0(,%rax,8), %r8 		# Set %r8 to &p[0]
	movq %r8, %rcx 				# Set %rcx to &p[0] (%rcx = p)
...
# Code for initialization loop
# i in %rax and on stack, n in %rdi, p in %rcx, q in %rdx
.L3: loop:
	movq %rdx, (%rcx,%rax,8) 	Set p[i] to q
	addq $1, %rax 				# Increment i
	movq %rax, -8(%rbp) 		# Store on stack
.L2:
	movq -8(%rbp), %rax 		# Retrieve i from stack
	cmpq %rdi, %rax 			# Compare i:n
	jl .L3 						# If <, goto loop
...
# Code for function exit
	## 释放栈空间
	leave 						# Restore %rbp and %rsp
	ret 						# Return

为了管理变长栈帧,x86-64使用寄存器 %rbp 作为帧指针(frame pointer)(也称基指针)。
1、因为 %rbp 本身是一个被调用者保存的寄存器,因此先把原存在 %rbp 的值保存到栈中;
2、在函数的整个执行过程中,都使得 %rbp 指向那个时刻的栈的位置,然后通过 %rbp 进行偏移实现对已分配栈空间的引用。

image-20211210213024230
(3)探讨

这里分配了多少空间?数组 p 被放在这段空间的什么位置?

【分析】
s1 表示执行 subq $16, %rsp 后栈顶指针的位置,这条指令实现了为局部变量 i 分配空间;
s2 表示执行 subq %rax, %rsp 后栈顶指针的位置,这条指令实现了为数组 p 分配空间;

s2 的计算逻辑如下:
s 1 − s 2 = ( 22 + 8 n ) & ( − 16 ) d = ( 22 + 8 n ) & ( 11110000 ) 2 = { 8 n + 8 , if  n  is even 8 n + 16 , if  n  is odd s1-s2=(22+8n){\&}(-16)_d=(22+8n){\&}(11110000)_2=\begin{cases}8n+8, & \text{if }n\text{ is even}\\ 8n+16,& \text{if }n\text{ is odd} \end{cases} s1s2=(22+8n)&(16)d=(22+8n)&(11110000)2={8n+8,8n+16,if n is evenif n is odd​​​​
——实现了向下舍入到最近的 16 的倍数
为什么是 16 的倍数?
因为栈帧边界默认是以 16 字节进行对齐的。【字节对齐是为了尽可能的提高CPU读取内存数据的速度】

【回答】
没有深入理解编译原理,而且不同编译器采取的机制也不尽相同。
所以到底分配了多少字节空间,我还是不知道。
数组 p 被放在这段空间的什么位置,我也还是不知道。

但是,仅就 CSPP 提供的🌰,可以确定答案(但目前仍是知其然,不知其所以然)
答案
(1)为数组 p 分配的空间大小
根据 n 的值计算编译器为了至少能够存放数组 p 而分配的空间大小(就是肯定是可以放得下数组 p 的);
(2)数组 p 在这个空间的什么位置
首先,跟踪代码的执行,先确定 s1 的值,然后 s1 减去分配的空间大小标量值,得到 s2
然后,还是根据字节对齐的思想,数组 p 是以指针为元素的数组,8字节对齐,指令 leaq 7(%rsp), %raxshrq $3, %raxleaq 0(,%rax,8), %r8 找到了距离 s2 最近的 8 的倍数的栈内地址,由此确定了数组的位置。

举例说明:已知 ns1,跟踪代码执行,确定其余的参数值。

ns1s2pe1e2
52065=2065-(8*5+8)=20172024,离2017最近的8的倍数=2065-2024-48=1=2024-2017=7
62064=2064-(8*6+16)=20002000=2064-2000-48=16=2000-2000=0
(4)leave 指令

leave 指令将帧指针恢复到它之前的值,没有操作数。

等价于:movq %rbp,%rsppopq %rbp

3.7.2 控制转移(Control Transfer)

——转移前后的指令地址处理

将控制从函数 P 转移到函数 Q 只需要将程序计数器(PC)设置为 Q 的代码的起始位置。在此之前,处理器必须记录好它需要继续 P 的执行的代码位置。

x86-64 中使用指令 call Q 调用过程 Q 来记录的:
把地址 A 压入栈中,并将 PC 设置为 Q 的起始地址;
压入的地址 A 被称为返回地址,是紧跟在 call 指令后的那条指令的地址;
对应 Q 的指令 ret 会从栈中弹出地址 A ,并把 PC 设置为 A 。

指令描述
call Label过程调用直接调用
call *Operand过程调用间接调用
ret从过程调用中返回

【举个🌰】

# 过程间传递控制的🌰,给出了两个函数top和leaf的反汇编代码
# Disassembly of leaf(long y)
# y in %rdi
0000000000400540 <leaf>:
	400540: 48 8d 47 02 	lea 0x2(%rdi),%rax 	# L1: z+2
	400544: c3 				retq 				# L2: Return
	
0000000000400545 <top>:
# Disassembly of top(long x)
# x in %rdi
	400545: 48 83 ef 05 	sub $0x5,%rdi 		# T1: x-5
	400549: e8 f2 ff ff ff 	callq 400540 <leaf> # T2: Call leaf(x-5)
	40054e: 4801c0 			add %rax,%rax 		# T3: Double result
	400551:	c3 				retq 				# T4: Return
# ...
# Call to top from function main
	40055b: e8 e5 ff ff ff 	callq 400545 <top> 	# M1: Call top(100)
	400560: 4889c2 			mov %rax,%rdx 		# M2: Resume
指令状态值(指令执行前)描述
标号PC指令%rdi%rax%rsp*%rsp
M10x40055bcallq1000x7fffffffe820Call top(100)
T10x400545sub1000x7fffffffe8180x400560进入 top
T20x400549callq950x7fffffffe8180x400560Call leaf(95)
L10x400540lea950x7fffffffe8100x40054e进入 leaf
L20x400544retq95970x7fffffffe8100x40054e从 leaf 返回 97
T30x40054eadd95970x7fffffffe8180x400560继续 top
T40x400551retq951940x7fffffffe8180x400560从 top 返回 194
M20x400560mov951940x7fffffffe820继续 main

3.7.3 数据传送(Data Transfer)

——转移前后的参数存储

call 指令执行前,将被调用的函数的参数必须已经存储在寄存器和栈空间。
在 x86-64 中:
△ 可以通过寄存器最多传递 6 个整型(整数和指针)参数( %rdi%rsi%rdx%rcx%r8%r9);
△ 可以通过寄存器最多传递 8 个浮点参数(%xmm0 ~ %xmm7)。
超过相应的寄存器个数,就需要使用栈空间来传递了。

寄存器的使用是有顺序的,寄存器使用的名字取决于要传递的数据类型的大小

通过栈传递参数时,所有的数据大小都以 8 字节对齐

【整型寄存器顺序、名字表】

操作数大小第1参数第2参数第3参数第4参数第5参数第6参数
64 位%rdi%rsi%rdx%rcx%r8%r9
32 位%edi%esi%edx%ecx%r8d%r9d
16 位%di%si%dx%cx%r8w%r9w
8 位%dil%sil%dl%cl%r8b%r9b

【浮点数寄存器顺序、名字表】

操作数第1参数第2参~第3参~第4参~第5参~第6参~第7参~第8参~
float→32位
double→64位
%xmm0%xmm1%xmm2%xmm3%xmm4%xmm5%xmm6%xmm7

【举个🌰】

假设过程 P 调用过程 Q ,Q 有 n 个整型参数,且 n>6。那么 P 代码分配的栈帧必须要能容纳 7 到 n 号参数的存储空间。
进一步地,假设过程 Q 也调用了某个有超过 6 个参数的函数 R,它也需要在自己的栈帧中为超出 6 个的参数分配空间,如下图的“参数构造区”。
栈的空间结构图如下:
image-20211211163031499

/* 参数传递的示例 */
void proc(long a1, long *a1p, 
          int a2, int *a2p, 
          short a3, short *a3p,
          char a4, char *a4p)
{
    *a1p += a1;
    *a2p += a2;
    *a3p += a3;
    *a4p += a4;
}
# void proc(a1, a1p, a2, a2p, a3, a3p, a4, a4p)
# Arguments passed as follows:
# a1  in %rdi (64 bits)
# a1p in %rsi (64 bits)
# a2  in %edx (32 bits)
# a2p in %rcx (64 bits)
# a3  in %r8w (16 bits)
# a3p in %r9  (64 bits)

# a4  at %rsp+8  (8  bits)
# a4p at %rsp+16 (64 bits)
proc:
	movq 16(%rsp), %rax 	# Fetch a4p (64 bits)
	addq %rdi, (%rsi) 		# *a1p += a1 (64 bits)
	addl %edx, (%rcx) 		# *a2p += a2 (32 bits)
	addw %r8w, (%r9) 		# *a3p += a3 (16 bits)
	movl 8(%rsp), %edx 		# Fetch a4 (8 bits)
	addb %dl, (%rax) 		# *a4p += a4 (8 bits)
	ret 					# Return

👆函数 proc 的参数 a4 和 a4p 通过栈传递。(8 字节对齐)

3.7.4 栈上的局部存储(Local Storage on the Stack)

在 3.7.3 小节中的栈内存结构图中的“局部变量”就属于“栈上的局部存储”。

【什么时候需要在栈上分配空间存储这些局部变量数据?】
常见情况包括:
(1)寄存器不足够存放所有的本地数据时;
(2)对一个局部变量使用地址运算符‘&’时,因为必须能够为其产生一个地址(这个地址当然是指内存地址。寄存器从存储角度来讲,和内存是一个级别的);
(3)局部变量是数组或结构时,因为数组和结构可以通过引用被访问,既需引用,则需要为其产生内存地址。

一般而言,过程通过减小栈顶指针在栈上获得空间,这个空间也是栈帧的一部分。

【举个🌰】

/* 该函数必须在栈上分配局部变量存储空间的函数
   还要向有8个参数的函数proc传递值			*/
long call_proc()
{
    long  x1 = 1;
    int   x2 = 2;
    short x3 = 3; 
    char  x4 = 4;
    proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
    return (x1+x2)*(x3-x4);
}
# long call_proc()
call_proc:
	# Set up arguments to proc
	subq $32, %rsp 			# Allocate 32-byte stack frame
	movq $1, 24(%rsp) 		# Store 1 in &x1
	movl $2, 20(%rsp) 		# Store 2 in &x2
	movw $3, 18(%rsp) 		# Store 3 in &x3
	movb $4, 17(%rsp) 		# Store 4 in &x4
	leaq 17(%rsp), %rax 	# Create &x4
	movq %rax, 8(%rsp) 		# Store &x4 as argument 8
	movl $4, (%rsp) 		# Store 4 as argument 7
	leaq 18(%rsp), %r9 		# Pass &x3 as argument 6
	movl $3, %r8d 			# Pass 3 as argument 5
	leaq 20(%rsp), %rcx 	# Pass &x2 as argument 4
	movl $2, %edx 			# Pass 2 as argument 314 
	leaq 24(%rsp), %rsi 	# Pass &x1 as argument 2
	movl $1, %edi 			# Pass 1 as argument 1
	# Call proc
	call proc
	# Retrieve changes to memory
	movslq 20(%rsp), %rdx 	# Get x2 and convert to long
	addq 24(%rsp), %rdx 	# Compute x1+x2
	movswl 18(%rsp), %eax 	# Get x3 and convert to int
	movsbl 17(%rsp), %ecx 	# Get x4 and convert to int
	subl %ecx, %eax 		# Compute x3-x4
	cltq 					# Convert to long
	imulq %rdx, %rax 		# Compute (x1+x2) * (x3-x4)
	addq $32, %rsp 			# Deallocate stack frame
	ret 					# Return

👆可以看到汇编代码中开始的一大部分,都是为调用函数 proc 做准备,其中包括为局部变量和函数参数建立栈帧,加载函数参数到寄存器中。

下图为函数 call_proc 的栈帧中用于局部存储的空间。

image-20211211171956632

3.7.5 需要在栈上保存的寄存器

一个过程中可能存在多个嵌套的过程,同一时刻,只有一个过程是活跃的
然而寄存器组是唯一被所有过程共享的资源

根据设计要求:
寄存器 %rbx%rbp%r12%r13%r14%r15 被划分为被调用者保存寄存器;
剩余的整型寄存器、全部的 XMM 浮点数寄存器都是调用者保存寄存器。

这就必须确保过程 P 调用过程 Q 时:
过程 P 必须先把调用者保存寄存器的值保存起来,就是压入过程 P 的栈帧中;
过程 Q 必须先把被调用者保存寄存器的值保存起来,就是压入过程 Q 的栈帧中,即下图中的“被保存的寄存器”部分。
(实际上,未被使用的,编译器压根不去理会)

image-20211211163031499

当函数包含指针、整数和浮点数混合的参数时,指针和整数通过通用寄存器传递,而浮点值通过 XMM 寄存器传递——参数到寄存器的映射取决于它们的类型和排列的顺序。
double f1(int x, double y, long z)x 存放在 %edi 中,y 放在 %xmm0 中,z 放在 %rsi 中;

3.7.6 递归过程(Recursive Procedures)

栈规则提供了一种机制:
每次函数调用都有其自己私有的状态信息(保存的返回值、被调用者保存寄存器的值、局部变量、调用者保存寄存器的值)。

在栈空间的分配上,递归调用、相互递归调用与调用其他函数是一样的。

🌰

/* 阶乘函数的递归实现 */
long rfact(long n)
{
    long result;
    if (n <= 1)
    	result = 1;
    else
    	result = n*rfact(n-1);
    return result;
}
# long rfact(long n)
# n in %rdi
rfact:
	pushq %rbx 				# Save %rbx
	movq %rdi, %rbx 		# Store n in callee-saved register4 
	movl $1, %eax 			# Set return value = 1
	cmpq $1, %rdi 			# Compare n:1
	jle .L35 				# If <=, goto done
	leaq -1(%rdi), %rdi 	# Compute n-1
	call rfact 				# Call rfact(n-1)
	imulq %rbx, %rax 		# Multiply result by n
.L35: 						# done:
	popq %rbx 				# Restore %rbx
	ret 					# Return
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
select * from (select t1.[id] as t1_id,t1.[requestId] as t1_requestId,t1.[htqsrq] as t1_htqsrq,t1.[htjzrq] as t1_htjzrq,t1.[htbh] as t1_htbh,t1.[gf] as t1_gf,t1.[xf] as t1_xf,t1.[rq] as t1_rq,t1.[fkfs] as t1_fkfs,t1.[formmodeid] as t1_formmodeid,t1.[modedatacreater] as t1_modedatacreater,t1.[modedatacreatertype] as t1_modedatacreatertype,t1.[modedatacreatedate] as t1_modedatacreatedate,t1.[modedatacreatetime] as t1_modedatacreatetime,t1.[modedatamodifier] as t1_modedatamodifier,t1.[modedatamodifydatetime] as t1_modedatamodifydatetime,t1.[form_biz_id] as t1_form_biz_id,t1.[MODEUUID] as t1_MODEUUID,t1.[htfj] as t1_htfj,t1.[zje] as t1_zje,t1.[ds] as t1_ds,t1.[zjedx] as t1_zjedx,t1.[cspp] as t1_cspp,t1.[yfk] as t1_yfk,t1.[gxid] as t1_gxid,t1.[bz] as t1_bz,t1.[gfqymc] as t1_gfqymc,t1.[gfjc] as t1_gfjc,t1.[bh] as t1_bh,t1.[jylx] as t1_jylx,t1.[cght] as t1_cght,t1.[yf] as t1_yf,t1.[yfk1] as t1_yfk1,t1.[yf11] as t1_yf11,t1.[nf] as t1_nf,t1.[rksj] as t1_rksj,t1.[cclx] as t1_cclx,t1.[cgbt] as t1_cgbt,t1.[yfk2] as t1_yfk2,t1.[sywf] as t1_sywf,t1.[yfbl] as t1_yfbl,t1.[fhbl] as t1_fhbl,t1.[yfh] as t1_yfh,t1.[sykf] as t1_sykf,t1.[hzsdlqys] as t1_hzsdlqys,t1.[sys_workflowid] as t1_sys_workflowid,t1.[cgqzyz] as t1_cgqzyz,t1.[htwjpdf] as t1_htwjpdf,t1.[cghtlc] as t1_cghtlc,t1.[htzt] as t1_htzt,t1.[qzfs] as t1_qzfs,t1.[htwjtp] as t1_htwjtp,t1.[cgqzlc] as t1_cgqzlc,t1.[sjfk] as t1_sjfk,t1.[ydkds] as t1_ydkds,t1.[chpt] as t1_chpt,t1.[lxdhchr] as t1_lxdhchr,t1.[gxsjkx] as t1_gxsjkx,t1.[hkzt] as t1_hkzt,t1.[lcfkd] as t1_lcfkd,t1.[fkzlcid] as t1_fkzlcid,t1.[mode_top_4] as t1_mode_top_4,t1.[cgdj] as t1_cgdj,t1.[mode_top_22] as t1_mode_top_22,t2.[id] as t2_id,t2.[mainid] as t2_mainid,t2.[sld] as t2_sld,t2.[ppcj] as t2_ppcj,t2.[hsdj] as t2_hsdj,t2.[bz] as t2_bz,t2.[je] as t2_je,t2.[xhggyt] as t2_xhggyt,t2.[mxgxid] as t2_mxgxid,t2.[dqkckc] as t2_dqkckc,t2.[rkhkc] as t2_rkhkc,t2.[yf] as t2_yf,t2.[yldjbhyf] as t2_yldjbhyf,SELECT year(rksj) as 年 FROM uf_gfht as cus_年年 from uf_gfht t1 INNER join uf_gfht_dt1 t2 on t1.id = t2.mainid) tmp1 where t1 错在哪里
05-14

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值