P1- 课程介绍
http://jyywiki.cn/ 该课程主要是通过手把手编写一个模拟器, 来了解计算机底层原理(编译,链接….) # P2-C语言拾遗 >总结: > - 编译链接过程 > - 预编译指令 > - 宏的使用 > - 内存模型 >

gcc hello.c // complier c file
vim a.out // see the bin file
:%!xxd // show with hexwe can see .ELF at the begin, this tell us this is an executeable file 我们可以看到开头的.ELF, 这表示这是一个(linux中)可执行文件
>可执行文件可以通过file打开
file a.out
上述过程直接通过
gcc实现了从.c到.out的过程 上述过程拆解: -gcc -S hello.c=> 生成.s汇编文件 -gcc -c hello.c=> 生成.o文件,但是没有链接 -objdump -d hello.o=> 将.o文件反汇编成我们可以看的懂的文件
在这里插入图片描述
预编译指令#
#include表示将代码复制粘贴进来
eg1
- hello.inc
printf("hello world\n");- hello.c
#include <stdio.h>
int main(int argc, char* argv[])
{
#include "hello.inc"
return 0;
} gcc -E hello.c
gcc --verbose hello.c 查看include文件是否在包含的目录中
可以看到#include "xxx"和#include <xxx>"的位置
>使用#include <xxx>"进行包含文件挥发现找不到这个文件, 系统目录中没有包含该文件. > 可以使用gcc -I./进行将目录添加到系统目录中
问题演示
解决方案
gcc -I. --verbose hello.c查看日志中是否包含目录
gcc -m32 hello.c 将项目编译为32位机器的目标文件
有趣的预编译, 因为aa和bb都没有定义所以都是NULL, NULL==NULL 得到True,所以输出yes
#include <stdio.h>
int main(int argc, char* argv[])
{
#if aa == bb
¦ printf("yes\n");
#else
¦ printf("no\n");
#endif
}
gcc -E aabb.c 查看预编译后的代码.
直接放入extern int printf (const char *__restrict __format, ...);不用全部include也是可以的
extern int printf (const char *__restrict __format, ...);
int main(int argc, char* argv[])
{
#if aa == bb
¦ printf("yes\n");
#else
¦ printf("no\n");
#endif
} 编译32位项目,出现报错:https://blog.csdn.net/wzzushx/article/details/119453099
sudo apt-get install build-essential module-assistant
sudo apt-get install gcc-multilib g++-multilib 预编译参数依旧添加进去了
extern int printf (const char *__restrict __format, ...);
int main(int argc, char* argv[])
{
#ifdef __x86_64__
¦ printf("__x86_64__\n");
#else
¦ printf("__x86__\n");
#endif
#if aa == bb
¦ printf("yes\n");
#else
¦ printf("no\n");
#endif
} 调用系统接口
system.c
#define SYSTEM sys ## tem
int main(){
SYSTEM("echo hello world\n");
}gcc system.c // 虽然这会报错, 但是没有影响
./a.out
hello world宏定义的展开
// 奇数行输出ture
#define true (__LINE__%2!=0)X-Macros
names.c
#include <stdio.h>
#define NAMES(X) X(TOM) X(Jerry) X(Lcc) X(Hig) X(lcu)
#define HELLO(M) M(Hello) M(world) M(\n)
int main(int argc, char* argv[])
{
#define PRINT(x) puts("hello," #x "!");
NAMES(PRINT)
HELLO(PRINT)
return 0;
} gcc -E names.c 使用gcc -E 进行查看预编译的代码。
# 5 "namex.c"
int main(int argc, char* argv[])
{
puts("hello," "TOM" "!"); puts("hello," "Jerry" "!"); puts("hello," "Lcc" "!"); puts("hello," "Hig" "!"); puts("hello," "lcu" "!");
puts("hello," "Hello" "!"); puts("hello," "world" "!"); puts("hello," "\n" "!");
return 0;
}编译&链接
连个函数文件,一个sum.c和main.c先对这两个文件进行编译,没有链接, 然后手动指定链接,最后运行a.out同样产生相同结果。
sum.c
int foo(int n){
int sum = 0;
for(int i=0;i<=n;i++){
¦ sum+=i;
}
return sum;
}main.c
#include <stdio.h>
int foo(int n);
int main(int argc, char* argv[])
{
printf("%d\n",foo(100));
return 0;
}
### 链接
gcc {main,sum}.o -static
## 内存 >总结 > - 代码存储的位置最低, 然后是只读变量(const修饰的变量), RO上面就是非零的变量, RW上边就是0变量和为被初始化的变量.上面就是堆栈. >
> 参考代码
#include <stdio.h>
void printptr(void *p){
printf("p = %p; *p = %016lx\n",p, *(long *)p);
}
char bss_1[10];
static int _static_1=65535;// ffff
static int _static_2;
int glab_1=65535;
int glab_2;
int glab_3=0;
const char _const = 'a';
int main(int argc,char *argv[]){
int b;
static int _static_3=65535;
static int _static_4=0;
printf("------- [code] ------\n");
printf("main:\t");
printptr(main); // 代码
printf("main:\t");
printptr(&main);
printf("------- [data] ------\n");
printf("_____________[data-RO]_____________\n");
printf("&const:\t");
printf("p = %p;\n",&_const);
printf("_____________[data-RW]_____________\n");
printf("stac_1:\t");
printptr(&_static_1);
printf("stac_2:\t");
printptr(&_static_2);
printf("stac_3:\t");
printptr(&_static_3);
printf("stac_4:\t");
printptr(&_static_4);
printf("glab_1:\t");
printptr(&glab_1); // 数据
printf("glab_2:\t");
printptr(&glab_2); // 数据
printf("glab_3:\t");
printptr(&glab_3);
printf("&bss_1:\t");
printf("p = %p;\n",&bss_1);
printf("b:\t");
printptr(&b);
printf("------- [heap] ------\n");
printf("\n");
printf("------- [stack] ------\n");
printf("argc:\t");
printptr(&argc); // 堆栈
printf("argv\t");
printptr(argv);
printf("&argv:\t");
printptr(&argv);
printf("argv[0:\t");
printptr(argv[0]);
}
# p3- C 语言拾遗(2): 编程实践 > 总结: > - 提升代码的可读性(Readable),明确代码的可维护性 > - 函数指针 > - 实战yemu(计算机系统运行原理)
代码的可读性和可维护性
通过signal的例子来说明如何提升代码的可读性.
// 这样的代码可读性就比较差(顺时针螺旋法则)
void (*signal (int sig, void (*func)(int)))(int);优化后的代码, 大大提升了代码的可阅读性
// 定义一个函数指针类型 sighandler_t, 表示他是一个接收参数数int的函数
typedef void (*sighandler_t)(int);
// 返回值是sighandler_t类型(返回函数类型),接收参数为一个int和一个接收为int的一个函数.
sighandler_t signal(int, sighandler_t);YEMU的原理
NEMU就是YEMU的增强.(通过YEMU的案例进一步说明代码的可读性和可维护性的重要性)
- 第2,3位表示 rt -> register target(猜测)目标寄存器,
- 第0,1位表示 rs -> register source (猜测)源寄存器
- 0123 表示的是指令的位置(第0,1,2,3位,4567类似)
- 0000 表示mov
- 0001 表示add
- 1110 表示load(默认加载到RA(R0)寄存器)
- 1111 表示store(默认将RA(R0)中的数据存储到制定地址)
> ## extern 将一个文件内的变量扩展到其他文件也可以使用
.
├── a.c // 变量所在位置
├── a.h
├── a.out
└── main.c // 将要使用的位置method-1
a.h
void hello(int num);
extern int num; // 添加外部变量a.c
#include <stdio.h>
int num = 100;
void hello(int num){
printf("%d\n",num);
}main.c
#include "a.h"
int main(void){
// method 2:在头文件中声明exter所需变量
// method 1: 解除下面的注释.
// extern int num;
int a = num;
hello(a);
return 0;
}method-2
a.h
void hello(int num);a.c
#include <stdio.h>
int num = 100;
void hello(int num){
printf("%d\n",num);
}main.c
#include "a.h"
int main(void){
extern int num;
int a = num;
hello(a);
return 0;
}union
将变量类型组合, 公用一个结构,个人感觉有点像是一个大的结构体(但是其所占用的大小是最大的那个变量的大小),将所有类型进行组合,大大提升代码的可阅读性和可维护性.
#include <stdio.h>
union data{
long age;
char ch;
};
int main(int argc, char* argv[])
{
union data stu1;
stu1.age = 18;
stu1.ch = 'a';
printf("union.age:%ld,sizeof:%ld\n",stu1.age,sizeof(stu1));
printf("union.ch:%d,sizeof:%ld\n",stu1.ch,sizeof(stu1));
return 0;
}对bit位进行操作,
#include <stdio.h>
typedef union inst{
// type(int is 16bit) part1:size, part2:size
struct {int a:8, b:8; } atype;
struct {int a:8, b:8; } btype;
}intst_t;
struct {int a:8, b:8; } ctype;
int main(int argc, char* argv[])
{
ctype.a = 0b01111111;
ctype.b = 0b10000011;
printf("%d %d size:%ld\n",ctype.a,ctype.b,sizeof(ctype));
return 0;
}MakeFile
使用makefile进行文件的构建对整个项目进行托管, 提升开发效率
.PHONY: run clean test
CFLAGS = -Wall -Werror -std=c11 -O2
CC = gcc
LD = gcc
# 构建yemu所需要yemu.o 和idex.o
yemu: yemu.o idex.o
# 使用的命令为:gcc -Wall -Werror -std=c11 -O2 -o yemu yemu.o idex.o
$(LD) $(LDFLAGS) -o yemu yemu.o idex.o
yemu.o: yemu.c yemu.h
$(CC) $(CFLAGS) -c -o yemu.o yemu.c
idex.o: idex.c yemu.h
$(CC) $(CFLAGS) -c -o idex.o idex.c
# make run
run: yemu
# @表示将该cmd不输出到终端
@./yemu
# make clean
clean:
# 使用make clean所执行的命令
rm -f test yemu *.o
# make test
test: yemu
$(CC) $(CFLAGS) -o test idex.o test.c && ./testp4 - NEMU框架选讲(1): 编译运行
总结: - unix 的优劣 - git的使用 一本说unix优劣书:https://web.mit.edu/~simsong/www/ugh.pdf
unix的关键字作为参数的问题
例如
rm -rf xxxx如果该命令删除一个名为-rf的文件该如何解决. 为了解决这个问题, linux的命令中出现了-- xxx- 尽管unix有些问题, 但是还是给我们带来了更多的便利
rm -rf -- -rf // 后续在linux上无法创建-开头的文件git的使用
git的出现正是由于程序员的工作需求诞生, 需要对代码进行版本管理,以及社区的维护都有非常好的管理能力. git学习地址(对git有一定了解提升下理解):https://marklodato.github.io/visual-git-guide/index-en.html 可视化git
工作区: 编辑代码的时候 暂存区: 执行git add 后的区域 仓库区: 执行git commit 后的区域
每次修改代码后可直接使用git diff 查看修改的地方, 如果使用git add 后则表示提交暂存区该代码保留, 此时再执行git diff 就没有任何不同了. git commit 将代码提交到了仓库, 表示作为一个版本.
交互模式
git {reset,checkout,add} -p // y(es), n(o), a(ll), q(quit),git diff
>### git commit git会使用与当前提交相同的父节点进行一次新提交,旧的提交会被替换。
git commit --amend
git commit --allow-emptygit checkout
checkout切换分支,切换文件。
将HEAD指到HEAD的前一个分支(前1个:^/~,前2个:^^/~~),但是没有创建分支,如果直接切换到其他分支,当前所指位置的文件修改将会丢失,如果需要保存,则需要进行创建新的分支(git checkout -b xxx)
git checkout HEAD^ 不保存
git restore 分离指针如果想要保存就需要进行创建新的分支。
git checkout -b test
git add .
git commit git merge
git merge 就是进行连个分支间进行迁移, 将当前的分支指向需要合并的分支。
git checkout master
git merge test // 将test分支的内容迁移到master分支
git rebase
rebase 就是将数据分支进行统一为线性,merge是将修改结果迁移,rebase是将所有的版本进行迁移。
>### git redo >- git reset 回到xxx节点 >- git revert 将xxx节点新建为后一个节点
Makefile的使用和解读
多线程编译
多个线程同时编译,大大多段时间
# 8线程同时编译
make -j8 强制编译
- 没有新的文件修改或产生也执行编译
- 多线程并行编译,加速编译
# 8线程同时编译
make -B
make -B -j8
# 计算时间,强制执行,8线程
time make -B -j8basename
获取当前文件名(获取路径的最后一个参数)
basename $(pwd)vi内置的bash
使用方法
!
:!echo hellomake -n
打印执行的命令
make -B
强制执行
解读NEMU的Makefile
- 查看make执行的命令
- 逐行解读
- grep参考地址
make -nB | greap -ve '^(\#\|echo\mkdir\)' | vim -
> ### 筛选数据
# -n 表示打印执行的命令
# -B always make
make -nB \
# grep 筛选
# ^ 行首
# -e 扩展正则
# -v 不包含
| grep -ve '^\(\#\|echo\|mkdir\)' \
# vim 从标准输出获取数据
| vim -调整格式
- set nowrap(不折叠)
将选中的部分:空格替换成换行 
处理完格式
- -D (definition) 就是在编译的过程中加上的宏
>### 链接
gcc
-O2 # 编译优化选项
-rdynamic # 动态链接
-o build/x86-nemu-interpreter # 最后输出到
build/obj-x86-interpreter/device/audio.o
build/obj-x86-interpreter/device/io/map.o
build/obj-x86-interpreter/device/io/port-io.o
build/obj-x86-interpreter/device/io/mmio.o
....
-lSDL2 # 图形库
-lreadline # 读行库
-ldl # 动态链接库Makefile转换成文档
doxygen
P5 - 框架代码选讲 (2):代码导读
查看文件树
tree ..
├── build
│ ├── obj-x86-interpreter
│ │ ├── device
│ │ │ ├── alarm.d
│ │ │ ├── alarm.o
│ │ │ ├── audio.d
│ │ │ ├── audio.o
│ │ │ ├── device.d
│ │ │ ├── device.o
│ │ │ ├── intr.d
│ │ │ ├── intr.o
│ │ │ ├── io
│ │ │ │ ├── map.d
│ │ │ │ ├── map.o
│ │ │ │ ├── mmio.d
│ │ │ │ ├── mmio.o
│ │ │ │ ├── port-io.d
│ │ │ │ └── port-io.o
│ │ │ ├── keyboard.d
│ │ │ ├── keyboard.o
│ │ │ ├── serial.d
....查找
.c和.h文件
find . -name "*.c" -o -name "*.h"统计代码行数
- xargs cat 将前面的参数传递xargs然后作为cat的输入
- wc -l 统计行数
# xargs cat 将前面的参数传递xargs然后作为cat的输入
find . -name "*.c" -o -name "*.h" | xargs cat | wc -l找到main函数
- grep -n [ with line num ]
- grep -e [ re ]
- grep -s [ no messge ]
查找main函数位置
find . -name "*.c" -o -name "*.h"| xargs grep --color -nse '\<main\>'
> ### fzf 查找( 奈斯 ) > - 可以配合其他命令使用
vim $(fzf)
cat $(fzf)
...static 解读
- static 所修饰的变量和函数作用域scope是当前的文件, 不可以跨越文件, 所以实现了可以重名的函数,和变量。
如下的案例,加深static 修饰时, f函数只能够调用到当前文件中的变量,或函数,无调用到b.c中的函数或者是static修饰的变量。所以能加上static修饰的函数尽量加上static进行修饰,否则实现跨文件的调用比较麻烦。
>### gcc -Wall -Werror > - 将warning转换为error 每一个报错都有意义, 身为合格的cser需要解决掉所有的warning和error
这里之所以会报错, 是因为在b.c中和函数没有使用,原本应该是warning这里因为加上了Werror所以将warning转换成了error

inline 解读
可以直接定义在头文件中,编译的时候后直接将代码粘贴过去,对性能需求有较高要求的应用比较适合。空间换时间
项目框架解读
进入到nemu的
src/main.c
init_monitor(argc, argv);随后进入到init_monitor里面的parse_args
- 这里调用了getopt这个库, 需要查看getopt这个手册–>
man getopt后详细信息查看man getopt.3或者man 3 getopt
static inline void parse_args(int argc, char *argv[]) {
const struct option table[] = {
{"batch" , no_argument , NULL, 'b'},
{"log" , required_argument, NULL, 'l'},
{"diff" , required_argument, NULL, 'd'},
{"port" , required_argument, NULL, 'p'},
{"help" , no_argument , NULL, 'h'},
{0 , 0 , NULL, 0 },
};
int o;
while ( (o = getopt_long(argc, argv, "-bhl:d:p:", table, NULL)) != -1) {
switch (o) {
case 'b': batch_mode = true; break;
case 'p': sscanf(optarg, "%d", &difftest_port); break;
case 'l': log_file = optarg; break;
case 'd': diff_so_file = optarg; break;
case 1:
if (img_file != NULL) Log("too much argument '%s', ignored", optarg);
else img_file = optarg;
break;
default:
printf("Usage: %s [OPTION...] IMAGE\n\n", argv[0]);
printf("\t-b,--batch run with batch mode\n");
printf("\t-l,--log=FILE output log to FILE\n");
printf("\t-d,--diff=REF_SO run DiffTest with reference REF_SO\n");
printf("\t-p,--port=PORT run DiffTest with port PORT\n");
printf("\n");
exit(0);
}
}
}make run
第一次运行大多会遇到
x86-nemu-interpreter: src/isa/x86/reg.c:20: reg_test: Assertion reg_w(i) == (sample[i] & 0xffff)' failed.
解决方法: 查看报错位置代码

工作区: 编辑代码的时候 暂存区: 执行git add 后的区域 仓库区: 执行git commit 后的区域