学号:sa****340 姓名:**钰
实验一:计算机是怎样工作的?
- 实验要求:请使用Example的C代码分别生成.cpp,.s,.o和ELF可执行文件,并加载运行,分析.s汇编代码的CPU上的执行过程。
- 实验报告要求:通过实验解释单任务计算机是怎样工作的,并在此基础上讨论分析多任务计算机是怎样工作的。
1 example.c源程序
#include <stdio.h>
int g(int x)
{
return x+3;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(8)+1;
}
计算机执行程序的过程是将源代码编译生成二进制文件,计算机最终能处理的只有二进制文件。编译器的工作过程如下:
(1)预处理(Pre-Processing)
(2)编译(Compiling)
(3)汇编(Assembling)
(4)连接(Linking)
2 具体执行过程
2.1 预处理
gcc -E -o example.cpp example.c
生成example.cpp文件
...
extern int fileno (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern int fileno_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
# 870 "/usr/include/stdio.h" 3 4
extern FILE *popen (__const char *__command, __const char *__modes) ;
extern int pclose (FILE *__stream);
extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
# 910 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 940 "/usr/include/stdio.h" 3 4
# 2 "example.c" 2
int g(int x)
{
return x+3;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(8)+1;
}
预处理的作用:- 宏的替换和常量标识符的替换
- 还有注释的消除
- 还有找到相关的库文件,将源文件中以”include”格式包含的文件复制到编译的源文件中
- 如果源代码中有预处理指令(如#if),那么预处理程序将先判断条件,再相应的修改源代码
- 用编辑器打开example.cpp会发现有很多很多代码,但是看最后部分就会发现,预处理做了宏的替换,还有注释的消除,可以理解为无关代码的清除。
在这个预处理阶段,源文件把include<stdio.h>代码编译进来,生成扩展的源文件example.cpp,这一步对程序员来说是透明的,预处理完成之后会自动将example.cpp进行编译。
2.2 编译
gcc -S -o example.s example.c
生成example.s文件
.file "example.c"
.text
.globl g
.type g, @function
g:
.LFB0:
.cfi_startproc
pushl %ebp ;保存old ebp,以便下次返回用
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp ;将esp的值赋给ebp,使得ebp指向新的一段活动记录
.cfi_def_cfa_register 5
movl 8(%ebp), %eax ;ebp +8地址中的参数赋给寄存器eax
addl $3, %eax ;eax寄存器的值加3
popl %ebp ;从栈中弹出ebp,使得ebp回复到原来的位置
.cfi_def_cfa 4, 4
.cfi_restore 5
ret ;返回eip,回到原来调用函数的下一条指令
.cfi_endproc
.LFE0:
.size g, .-g
.globl f
.type f, @function
f:
.LFB1:
.cfi_startproc
pushl %ebp ;保存old ebp,以便下次返回用
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp ;将esp的值赋给ebp,使得ebp指向新的一段活动记录
.cfi_def_cfa_register 5
subl $4, %esp ;esp向栈底开辟4个字节的空间
movl 8(%ebp), %eax ;ebp +8地址中的参数赋给寄存器eax
movl %eax, (%esp) ;将寄存器eax中的值赋给esp所指向的空间
call g ;调用函数g
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret ;返回eip,回到原来调用函数的下一条指令
.cfi_endproc
.LFE1:
.size f, .-f
.globl main
.type main, @function
main:
.LFB2:
.cfi_startproc
pushl %ebp ;保存old ebp,以便下次返回用
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp ;将esp的值赋给ebp,使得ebp指向新的一段活动记录
.cfi_def_cfa_register 5
subl $4, %esp ;esp向栈底开辟4个字节的空间
movl $8, (%esp) ;ebp +8地址中的参数赋给寄存器eax
call f ;调用函数f
addl $1, %eax ;eax寄存器的值加1
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret ;返回eip,回到原来调用函数的下一条指令
.cfi_endproc
.LFE2:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
.section .note.GNU-stack,"",@progbits
在上述文件中多处出现以.cfi开头的命令,虽然通过查阅一定资料,但是还是不是很理解,可以参考这里。
程序执行过程中,内存的变化如下:正向调用过程如下:
当函数调用结束时回复到调用函数的下一条指令的地方,这时候内存的变化,%ebp和%esp我没有画图显示。
从上面的.s文件可以看出,每一个函数的开头总是:
pushl %ebp ;保存old ebp,以便下次返回用
movl %esp, %ebp ;将esp的值赋给ebp,使得ebp指向新的一段活动记录
movl %esp, %ebp ;将esp的值赋给ebp,使得ebp指向新的一段活动记录
把ebp压入栈中,是为了在函数返回的时候便于恢复以前的ebp值。而之所以要保存一些寄存器,在于编译器可能要求某些寄存器在调用前后保持不变,那么函数就可以在函数开始时将这些寄存器的值压入栈中,在结束时取出。所以在函数返回时,所进行的标准结尾与标准开头正好相反。
movl %ebp,%esp
pop %ebp
2.3 汇编
gcc -c -o example.o example.c
as -o example.o example.s
汇编阶段把编译阶段生成的example.s文件转换为二进制目标文件,也就是翻译成机器语言指令,把这些指令打包成可重定为目标程序的格式。
生成example.o文件
2.4 连接
gcc -o example example.o
生成可执行文件example,没有后缀名。
在终端可以执行:./example
可以用file 命令查看example的属性。
终端执行命令:file example
显示如下:
songzeyu@ubuntu:~/Linux system/lab1$ file example
example: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, not stripped
example: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, not stripped
其中的executable指出该文件为ELF中的可执行文件类型。
3 计算机工作模式:单任务和多任务的浅析
3.1 单任务的工作模式
计算机在同一时间只能运行一个应用程序,计算机首先从外存中加载程序到内存,然后依次执行程序指令,完全执行完毕之后才可以加载、执行下一个程序。具体的执行过程如上面所分析得一样。
单任务的缺点,由于CPU资源十分昂贵,如果我们需要优先运行级别较高的程序,单任务是不能满足要求的。
3.2 多任务的工作模式
计算机在同一时间可以运行多个应用程序,现在的操作系统基本上都是多任务工作模式的。同时运行的多个程序之前不会相互干扰,但是让CPU同时运行多个程序,必须使用并行技术,最容易理解的就是“时间片轮转进程调度算法”。
4 实验小结
PS:这次实验咋一看也许觉得很简单,但是当我真正去做的时候,发现其中的很多细节的知识点很模糊。计算机是如何工作这本身就不是一个简单的问题,就算是科班出生的同学也并不一定能正确的解释它的运行机制。所以从实验的前期准备到开始做实验我都很认真。
之前尽管知道程序运行的过程为:预处理、编译、汇编、连接,但是对其中的每一步是如何实现的,如何进行命名的操作都不是很清楚,平时运行程序只要一行命令就可以完成:gcc example -o example.c。通过这次实验我掌握了分别进行操作。在这之前我查阅了一些资料用于这次实验:
编译器的工作模式:
0、 gcc -o example example.c 不加 -c -S -E 参数,编译器将执行预处理/编译/汇编/链接操作直接生成可执行代码。
1、 gcc -c -o example.o example.c的作用
-c 参数将对源程序example.c进行预处理,编译,汇编操作,生成example.o文件。去掉指定输出选项"-o example.o"自动输出为example.o,所以这里-o加不加都可以。
2、 gcc -S -o example.s example.c的作用
-S参数将对源程序example.c进行预处理,编译,生成example.s文件。-o选项同上。
3、 gcc -E -o example.i example.c的作用
-E 参数将对源程序example.c进行预处理,生成 example.i文件(不同版本不宜样),就是将#include ,#define 等进行文件插入及宏扩展等操作。
4、 gcc -v -o example example.c的作用
虽然这些都是基本知识,可是在实验之前我并不了解。
加上-v 参数,显示编译时的详细信息,编译器的版本,编译过程等。
5、gcc -Wall -o example example.c
-Wall 选项打开了所有需要注意的警告信息,像在声明之前就使用的函数,声明后却没有使用的变量等。
6、gcc -Ox -o example example.c
另一方面通过这次实验我对栈的理解有了深一步的理解和掌握。
5 参考资料
5.1 《程序员的自我修养—链接、装载与库》
这是我目前看到的将这方面的知识比较好的一本书,我主要看了第十章“内存”,里面图文并茂的讲解给这次实验帮了不少的忙。
5.2 《深入理解计算机系统》
我主要看了第三章“程序的机器级表示”,核心的知识点讲了程序在堆栈中的表示。