实验一 ID SA12226267
首先,概述一下编译链接的过程:C语言的编译链接过程是将程序源代码转换成可以运行的可执行文件,需要进行编译和链接。编译就是把文本形式源代码(高级语言指令)转换为功能等效的汇编代码。链接是把目标文件、操作系统的启动代码和用到的库文件链接最终生成二进制可执行代码的过程。完整过程可以描述如下():
C源程序--->预处理(.c)--->编译(.s)--->汇编(.o)--->链接(可执行文件)
源代码Example.c
int g(int x)
{
return x + 3;
}
int f(int x)
{
return g(x);
}
int main()
{
return f(8) + 1;
}
进行预处理,预处理后的文件为Example.cpp ,gcc -E Example.c -o Example.cpp,用vim Example.cpp可看到预处理后的文件
# 1 "Example.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "Example.c"
int g(int x)
{
return x + 3;
}
int f(int x)
{
return g(x);
}
int main()
{
return f(8) + 1;
}
然后是编译成汇编代码
gcc -x cpp-output -S -o Example.s Example.cpp ,其中参数-x说明根据指定的步骤进行工作,cpp-output指明从预处理得到的文件开始编译,-S说明生成汇编代码后停止工作。生成的汇编代码如下;
.file "Example.c"
.text
.globl g
.type g, @function
g:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
movl 8(%ebp), %eax
addl $3, %eax
popl %ebp
.cfi_def_cfa 4, 4
.cfi_restore 5
ret
.cfi_endproc
.LFE0:
.size g, .-g
.globl f
.type f, @function
f:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call g
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size f, .-f
.globl main
.type main, @function
main:
.LFB2:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $4, %esp
movl $8, (%esp)
call f
addl $1, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE2:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
.section .note.GNU-stack,"",@progbits
汇编生成目标代码 gcc -x assembler -c Example.s -o Example.o
最后生成可执行文件gcc -o Example.o Example
涉及的几个寄存器有ebp,esp,eip,其中ebp和esp分别指向当前栈的栈底和栈顶,eip是指令寄存器,存储着CPU要执行的下条指令的地址。
此外,还需要注意几个汇编指令:
call 指令:执行call 指令时:会把当前eip的值压栈保存,并使得eip等于被调用函数的起始地址。
leave指令:执行leave指令,等于下面两条指令:
movl %ebp , %esp //使栈顶指针指向栈基指针
pop %ebp //使得栈基指针恢复为前一次保存的ebp的值
ret指令:等于 pop %ebp 即恢复ebp的值。
从main函数开始分析:
pushl %ebp
movl %esp, %ebp
显示ebp入栈(保存之前的ebp),然后使ebp指向与esp相同,此时栈情况如下:
调用函数之前,要将参数(本例是8)压栈,
subl $4, %esp
movl $8, (%esp)
这两条指令完成参数入栈的作用,此时,栈的情况如下图:
然后是call f;
call f 的过程包括将eip入栈,即保存返回地址,然后将函数f的地址给eip,然后开始执行函数f
执行函数f的过程,首先仍然是保存旧栈,
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
这个过程是参数x(这里是8)入栈,执行完后
接着是call g,
然后开始执行g,同理,先保存ebp,
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $3, %eax
这两条指令进行计算,计算结果放入eax寄存器,内存不发生变化
popl %ebp
ret
完成计算后,出栈
此时,返回到f中,
leave
ret
其中,leave等价于movl %ebp , %esp;popl %ebp;
ret 相当于popl %eip
再次回到main中。
addl $1, %eax
该指令进行计算,结果仍然存在eax中。
leave
ret
函数执行完毕。
关于计算机单任务与多任务的运行:
单任务运行,CPU通过读取EIP寄存器的值得值下一条指令的内存地址,然后读取指令并执行。整个过程中,始终由EIP保存将要执行的下一条指令的地址。函数调用过程中,如上面分析,需要保存现场,执行完后要返回,主要是通过EBP,ESP等寄存器实现的。
多任务情况要复杂一些,依靠进程调度、中断机制等实现。