linux下的程序运行分析
linux下的程序运行分析
Linux系统模型
内核是操作系统的核心,具有很多最基本的功能,包括进程管理、内存管理、设备驱动管理、文件系统和网络管理等等。整体的架构如下图所示。
内存管理
对任何一台计算机而言,其内存以及其它资源都是有限的。为了让有限的物理内存满足应用程序对内存的大需求量,Linux 采用了称为“虚拟内存”的内存管理方式。Linux 将内存划分为容易处理的“内存页”(对于大部分体系结构来说都是 4KB)。Linux 包括了管理可用内存的方式,以及物理和虚拟映射所使用的硬件机制。
进程管理
进程实际是某特定应用程序的一个运行实体。在 Linux 系统中,能够同时运行多个进程,Linux 通过在短的时间间隔内轮流运行这些进程而实现“多任务”。这一短的时间间隔称为“时间片”,让进程轮流运行的方法称为“进程调度” ,完成调度的程序称为调度程序。
进程调度控制进程对CPU的访问。当需要选择下一个进程运行时,由调度程序选择最值得运行的进程。可运行进程实际上是仅等待CPU资源的进程,如果某个进程在等待其它资源,则该进程是不可运行进程。Linux使用了比较简单的基于优先级的进程调度算法选择新的进程。
文件系统
虚拟文件系统(VirtualFileSystem,VFS):隐藏了各种硬件的具体细节,把文件系统操作和不同文件系统的具体实现细节分离了开来,为所有的设备提供了统一的接口,VFS提供了多达数十种不同的文件系统。虚拟文件系统可以分为逻辑文件系统和设备驱动程序。逻辑文件系统指Linux所支持的文件系统,如ext2,fat等,设备驱动程序指为每一种硬件控制器所编写的设备驱动程序模块。
设备驱动程序
设备驱动程序是 Linux 内核的主要部分。和操作系统的其它部分类似,设备驱动程序运行在高特权级的处理器环境中,从而可以直接对硬件进行操作,但正因为如此,任何一个设备驱动程序的错误都可能导致操作系统的崩溃。设备驱动程序实际控制操作系统和硬件设备之间的交互。
设备驱动程序提供一组操作系统可理解的抽象接口完成和操作系统之间的交互,而与硬件相关的具体操作细节由设备驱动程序完成。一般而言,设备驱动程序和设备的控制芯片有关,例如,如果计算机硬盘是 SCSI 硬盘,则需要使用 SCSI 驱动程序,而不是 IDE 驱动程序。
网络接口
提供了对各种网络标准的存取和各种网络硬件的支持。网络接口可分为网络协议和网络驱动程序。网络协议部分负责实现每一种可能的网络传输协议。众所周知,TCP/IP 协议是 Internet 的标准协议,同时也是事实上的工业标准。
Linux 的网络实现支持 BSD 套接字,支持全部的TCP/IP协议。Linux内核的网络部分由BSD套接字、网络协议层和网络设备驱动程序组成。网络设备驱动程序负责与硬件设备通讯,每一种可能的硬件设备都有相应的设备驱动程序。
程序运行实例分析
下面对linux下一段golang程序进行分析:
package main
func main() {
a := 3
b := 2
sum(a, b)
}
func sum(a, b int) int {
result := a + b
return result
}
寄存器
在go语言中,函数的参数和返回值都是通过栈来传递的,函数调用时会有自己的栈帧,并引入了几个寄存器来定位栈帧的大小和值的调用。
- BP(base pointer):基址指针寄存器,也叫帧指针,表示函数栈开始的地方。
- SP(stack pointer):栈指针寄存器,表示函数栈空间的栈顶,函数栈结束的地方。
由于栈在内存中是由高地址往低地址扩张,所以BP在高地址,而SP在低地址。
BP和SP都是硬件寄存器,在golang的汇编Plan9中,还引入了一些伪寄存器。
- FP(frame pointer):伪FP寄存器,指向调用函数的栈顶,用于操作参数和返回值。
- SP(pseudo stack pointer):伪SP寄存器,其实就是BP寄存器,也指向栈底,用来操作局部变量。
伪FP和伪SP不是硬件寄存器,只是方便对参数,返回值,和局部变量的控制. 可以通过偏移量和SP进行联系,转为机器指令。
执行过程分析
编译
通过go自带的命令可以输出编译后的汇编指令。
go tool compile -S -N -l test.go
以下为输出的结果,已经删去了一些与程序主体无关的指令。
"".main STEXT size=94 args=0x0 locals=0x30
0x0000 00000 (test.go:3) TEXT "".main(SB), ABIInternal, $48-0
......
0x0016 00022 (test.go:3) SUBQ $48, SP //分配栈帧
0x001a 00026 (test.go:3) MOVQ BP, 40(SP) //保存旧的BP
0x001f 00031 (test.go:3) LEAQ 40(SP), BP //分配新的BP
......
0x0024 00036 (test.go:4) MOVQ $3, "".a+32(SP) //为局部变量a赋值
0x002d 00045 (test.go:5) MOVQ $2, "".b+24(SP) //为局部变量b赋值
0x0036 00054 (test.go:6) MOVQ "".a+32(SP), AX //把a的值赋给AX寄存器
0x003b 00059 (test.go:6) MOVQ AX, (SP) //传入参数a
0x003f 00063 (test.go:6) MOVQ $2, 8(SP) //传入参数b
0x0048 00072 (test.go:6) CALL "".sum(SB) //调用sum函数
0x004d 00077 (test.go:7) MOVQ 40(SP), BP //把旧BP的值赋给BP寄存器
0x0052 00082 (test.go:7) ADDQ $48, SP //释放main函数栈帧空间
0x0056 00086 (test.go:7) RET //main函数返回,执行结束
......
"".sum STEXT nosplit size=52 args=0x18 locals=0x10
0x0000 00000 (test.go:9) TEXT "".sum(SB), NOSPLIT|ABIInternal, $16-24
0x0000 00000 (test.go:9) SUBQ $16, SP //分配栈帧
0x0004 00004 (test.go:9) MOVQ BP, 8(SP) //保存main函数的BP
0x0009 00009 (test.go:9) LEAQ 8(SP), BP //分配新的BP
......
0x000e 00014 (test.go:9) MOVQ $0, "".~r2+40(SP) //把返回值置为0
0x0017 00023 (test.go:10) MOVQ "".a+24(SP), AX //把参数a的值赋给AX寄存器
0x001c 00028 (test.go:10) ADDQ "".b+32(SP), AX //执行a+b
0x0021 00033 (test.go:10) MOVQ AX, "".result(SP) //把a+b的结果赋给局部变量result
0x0025 00037 (test.go:11) MOVQ AX, "".~r2+40(SP) //把a+b的结果赋给返回值
0x002a 00042 (test.go:11) MOVQ 8(SP), BP //把main函数的BP值赋给BP寄存器
0x002f 00047 (test.go:11) ADDQ $16, SP //释放sum函数栈帧空间
0x0033 00051 (test.go:11) RET //sum函数返回
main函数执行分析
0x0000 00000 (test.go:3) TEXT "".main(SB), ABIInternal, $48-0
TEXT
表示这是一段可执行指令,是代码段,"".main(SB)
表示函数名,SB也是一个伪寄存器,可以理解为用来表示不同的标识符。ABIInternal
是Plan9中的标志,不用去理。
最后的$48-0
表示了函数栈帧的大小以及参数,返回值的大小。main
函数中定义了两个整形局部变量a
和b
,并调用了sum
函数,则在栈帧中需要有两个整形参数和一个整形返回值。也就是5个整形变量,共5*8=40个字节。另外,还需要8个字节保存调用者的BP值,对main
函数来说没有调用者,但也需要保存BP,所以总共就是48个字节。main
函数没有参数和返回值,为0。
0x0016 00022 (test.go:3) SUBQ $48, SP //分配栈帧
0x001a 00026 (test.go:3) MOVQ BP, 40(SP) //保存旧的BP
0x001f 00031 (test.go:3) LEAQ 40(SP), BP //分配新的BP
之后开始真正的指令.。
- 首先把SP减少48,相当于分配了48字节的栈帧空间;
- 在保存之前的BP寄存器的值;
- 最后让BP指向当前栈帧的底部。
这三条指令是每个函数调用都需要执行的:分配栈帧空间,保存就BP,分配新BP
0x0024 00036 (test.go:4) MOVQ $3, "".a+32(SP) //为局部变量a赋值
0x002d 00045 (test.go:5) MOVQ $2, "".b+24(SP) //为局部变量b赋值
0x0036 00054 (test.go:6) MOVQ "".a+32(SP), AX //把a的值赋给AX寄存器
0x003b 00059 (test.go:6) MOVQ AX, (SP) //传入参数a
0x003f 00063 (test.go:6) MOVQ $2, 8(SP) //传入参数b
接着为局部变量赋值,并传递参数。可以参照下图进行理解。
0x0048 00072 (test.go:6) CALL "".sum(SB) //调用sum函数
之后用CALL
指令调用sum
函数,CALL
指令其实是可以分解为两条命令。
SUBQ $8, SP
MOVQ IP, (SP)
首先让栈帧扩大8个字节,并把main
的CALL
指令地址送入刚扩的空间中,便于函数返回时继续执行指令。
sum函数执行分析
0x0000 00000 (test.go:9) TEXT "".sum(SB), NOSPLIT|ABIInternal, $16-24
和main
函数一样,sum
也有自己的栈帧,为16个字节,包括保存main
的BP和一个局部整形变量result
。
另外,sum
函数是个有参数和返回值的函数,三个整形变量共24个字节。
0x0000 00000 (test.go:9) SUBQ $16, SP //分配栈帧
0x0004 00004 (test.go:9) MOVQ BP, 8(SP) //保存main函数的BP
0x0009 00009 (test.go:9) LEAQ 8(SP), BP //分配新的BP
这三条指令和main
一样。
0x000e 00014 (test.go:9) MOVQ $0, "".~r2+40(SP) //把返回值置为0
0x0017 00023 (test.go:10) MOVQ "".a+24(SP), AX //把参数a的值赋给AX寄存器
0x001c 00028 (test.go:10) ADDQ "".b+32(SP), AX //执行a+b
0x0021 00033 (test.go:10) MOVQ AX, "".result(SP) //把a+b的结果赋给局部变量result
0x0025 00037 (test.go:11) MOVQ AX, "".~r2+40(SP) //把a+b的结果赋给返回值
把a
和b
相加送入"".~r2+40(SP)
,也就是给main
函数传递返回值。
0x002a 00042 (test.go:11) MOVQ 8(SP), BP //把main函数的BP值赋给BP寄存器
0x002f 00047 (test.go:11) ADDQ $16, SP //释放sum函数栈帧空间
0x0033 00051 (test.go:11) RET //sum函数返回
返回值传递结束后,需要回收栈帧。
- 把调用者的BP赋值给BP寄存器;
- SP指针增大,指向调用者栈帧顶部;
- 调用
RET
指令,返回调用函数。
同CALL
指令一样,RET
指令也可以分解为两步执行。
LEAQ (SP), IP
ADDQ $8, SP
0x004d 00077 (test.go:7) MOVQ 40(SP), BP //把旧BP的值赋给BP寄存器
0x0052 00082 (test.go:7) ADDQ $48, SP //释放main函数栈帧空间
0x0056 00086 (test.go:7) RET //main函数返回,执行结束
最后再执行main
的退栈和返回过程,整个程序执行结束。