SA420 马林
【实验一】计算机是怎样工作的?
- 请使用Example的c代码分别生成.cpp,.s,.o和ELF可执行文件,并加载运行,分析.s汇编代码在CPU上的执行过程
- 实验报告要求:通过实验解释单任务计算机是怎样工作的,并在此基础上讨论分析多任务计算机是怎样工作的。
Example:
int g(int x)
{
return x+3;
}
int f(int x)
{
return g(x);
}
int main(void)
{
returnf(8)+1;
}
一、预编译
将example.c预编译成example.cpp
指令:gcc -E -o example.cpp example.c
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(void)
{
returnf(8)+1;
}
可以看到,预编译将原本的#include进行了替换,讲呗包含的未见插入到了该预编译指令的位置,并且添加了行号和文件名标志。如:
# 1 "example.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "example.c"
里的#1表示#include语句所在的行数为1,文件名为example.c
二、编译成汇编代码
将example.cpp编译成汇编代码example.s
指令:gcc -x cpp-output -S -o lab1.slab1.cpp
example.s:
.file "example.c"
.text
.globl g
.type g, @function
g: #g函数入口
.LFB0:
.cfi_startproc
pushl %ebp #将ebp寄存器中的地址压栈
.cfi_def_cfa_offset8
.cfi_offset 5,-8
movl %esp, %ebp #将栈顶地址赋给栈底
.cfi_def_cfa_register5
movl 8(%ebp), %eax #ebp寄存器的加8后的内容付给eax寄存器
addl $3, %eax #将立即数3加上eax寄存器中内容再赋给eax寄存器
popl %ebp 出栈内容付给ebp寄存器
.cfi_def_cfa 4,4
.cfi_restore 5
ret # 将栈顶内容赋给eip寄存器
.cfi_endproc
.LFE0:
.size g, .-g
.globl f
.type f, @function
f: #f函数入口
.LFB1:
.cfi_startproc
pushl %ebp #将ebp寄存器的内容压栈
.cfi_def_cfa_offset8
.cfi_offset 5,-8
movl %esp, %ebp #将esp寄存器的内容赋给ebp寄存器
.cfi_def_cfa_register5
subl $4, %esp #将esp寄存器内容减4
movl 8(%ebp), %eax #ebp寄存器内容加8后付给eax寄存器
movl %eax, (%esp) #将eax寄存器内容赋给esp寄存器内容指向的地址
call g #等价于pushl %eip;move g %eip
leave #等价于movel %ebp %esp; popl %ebp
.cfi_restore 5
.cfi_def_cfa 4,4
ret #等价于popl %eip
.cfi_endproc
.LFE1:
.size f, .-f
.globl main
.type main, @function
main: #main函数入口
.LFB2:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset8
.cfi_offset 5,-8
movl %esp, %ebp
.cfi_def_cfa_register5
subl $4, %esp #将esp寄存器里的地址减少4
movl $8, (%esp) #将立即数8压栈
call f
addl $1, %eax #eax寄存器里的值+1
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
三、生成目标文件example.o
指令:gcc -x assembler -c example.s -o example.o
首先用指令:gdpdump -h example.o将目标文件的各个段基本信息打印出来。
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000035 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 00000000 00000000 0000006c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 0000006c 2**2
ALLOC
3.comment 0000002b 00000000 00000000 0000006c 2**0
CONTENTS, READONLY
4.note.GNU-stack 00000000 00000000 00000000 00000097 2**0
CONTENTS, READONLY
5.eh_frame 00000078 00000000 00000000 00000098 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
其中,SIZE为段的大小,FileOff为段在文件中的位置。由此我们可以了解到此文件的段分布情况。
再由指令:objdump -d example.o可以查看代码段汇编。
example.o: file format elf32-i386
Disassembly of section .text:
00000000 <g>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 45 08 mov 0x8(%ebp),%eax
6: 83 c0 03 add $0x3,%eax
9: 5d pop %ebp
a: c3 ret
0000000b <f>:
b: 55 push %ebp
c: 89 e5 mov %esp,%ebp
e: 83 ec 04 sub $0x4,%esp
11: 8b 45 08 mov 0x8(%ebp),%eax
14: 89 04 24 mov %eax,(%esp)
17: e8 fc ff ff ff call 18 <f+0xd>
1c: c9 leave
1d: c3 ret
0000001e <main>:
1e: 55 push %ebp
1f: 89 e5 mov %esp,%ebp
21: 83 ec 04 sub $0x4,%esp
24: c7 04 24 08 00 00 00 movl $0x8,(%esp)
2b: e8 fc ff ff ff call 2c <main+0xe>
30: 83 c0 01 add $0x1,%eax
33: c9 leave
34: c3 ret
图1
图2
图3
图4
图5
四、生成ELF可执行文件
指令:gcc -o example example.c
总结:从C代码到可执行的ELF文件之间,经历了各个阶段。
在预编译阶段,编译器将#include文件插入到代码相应的位置中;去掉了注释;展开了宏;为生产汇编代码做好了准备。
在编译阶段,编译器将预处理后的代码文件生成了相应的汇编代码文件。
在汇编阶段,编译器将汇编代码变成机器可以执行的指令,每一个汇编语句
在汇编阶段,编译器将汇编代码变成机器可以执行的指令,每一个汇编语句几乎对应相应的机器语言,生成.o的目标文件
最后,生成ELF文件。
对于单任务的计算机而言,计算机按照最终生成的ELF文件循环进行“取指,执行”的过程即可。函数调用call的过程是逐级压栈,开辟新栈,保存现场,将当前eip压栈,将目标地址放入eip寄存器中。函数返回ret过程是逐级出栈,将原本的eip地址出栈放入eip中。
对于多任务的计算机而言,其实本质也并没有太大的区别。计算机依旧循环进行“取指,执行”。只是每个进程信息由PCB(进程控制模块)保管。进程与进程之间分时切换,切换前需要保存现场信息(如pid,eip,ebp等)内容更多,同时读取下一个时间片被执行进程的重要信息。