P1- 课程介绍
http://jyywiki.cn/ 该课程主要是通过手把手编写一个模拟器, 来了解计算机底层原理(编译,链接….) # P2-C语言拾遗 >总结: > - 编译链接过程 > - 预编译指令 > - 宏的使用 > - 内存模型 >
gcc hello.c // complier c file
vim a.out // see the bin file
:%!xxd // show with hex
we 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
("hello world\n"); printf
- 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
("yes\n");
¦ printf#else
("no\n");
¦ printf#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
("yes\n");
¦ printf#else
("no\n");
¦ printf#endif
}
编译32位项目,出现报错:https://blog.csdn.net/wzzushx/article/details/119453099
-get install build-essential module-assistant
sudo apt-get install gcc-multilib g++-multilib sudo apt
预编译参数依旧添加进去了
extern int printf (const char *__restrict __format, ...);
int main(int argc, char* argv[])
{
#ifdef __x86_64__
("__x86_64__\n");
¦ printf#else
("__x86__\n");
¦ printf#endif
#if aa == bb
("yes\n");
¦ printf#else
("no\n");
¦ printf#endif
}
调用系统接口
system.c
#define SYSTEM sys ## tem
int main(){
("echo hello world\n");
SYSTEM}
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 "!");
(PRINT)
NAMES(PRINT)
HELLOreturn 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++){
+=i;
¦ sum}
return sum;
}
main.c
#include <stdio.h>
int foo(int n);
int main(int argc, char* argv[])
{
("%d\n",foo(100));
printfreturn 0;
}
### 链接
{main,sum}.o -static gcc
## 内存 >总结 > - 代码存储的位置最低, 然后是只读变量(
const
修饰的变量), RO上面就是非零的变量, RW上边就是0变量和为被初始化的变量.上面就是堆栈. > > 参考代码
#include <stdio.h>
void printptr(void *p){
("p = %p; *p = %016lx\n",p, *(long *)p);
printf}
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;
("------- [code] ------\n");
printf("main:\t");
printf(main); // 代码
printptr("main:\t");
printf(&main);
printptr
("------- [data] ------\n");
printf("_____________[data-RO]_____________\n");
printf("&const:\t");
printf("p = %p;\n",&_const);
printf("_____________[data-RW]_____________\n");
printf("stac_1:\t");
printf(&_static_1);
printptr("stac_2:\t");
printf(&_static_2);
printptr("stac_3:\t");
printf(&_static_3);
printptr("stac_4:\t");
printf(&_static_4);
printptr
("glab_1:\t");
printf(&glab_1); // 数据
printptr("glab_2:\t");
printf(&glab_2); // 数据
printptr("glab_3:\t");
printf(&glab_3);
printptr("&bss_1:\t");
printf("p = %p;\n",&bss_1);
printf("b:\t");
printf(&b);
printptr("------- [heap] ------\n");
printf
("\n");
printf
("------- [stack] ------\n");
printf("argc:\t");
printf(&argc); // 堆栈
printptr("argv\t");
printf(argv);
printptr("&argv:\t");
printf(&argv);
printptr("argv[0:\t");
printf(argv[0]);
printptr}
# 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的一个函数.
(int, sighandler_t); sighandler_t signal
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 将一个文件内的变量扩展到其他文件也可以使用
.
.c // 变量所在位置
├── a.h
├── a.out
├── a.c // 将要使用的位置 └── main
method-1
a.h
void hello(int num);
extern int num; // 添加外部变量
a.c
#include <stdio.h>
int num = 100;
void hello(int num){
("%d\n",num);
printf}
main.c
#include "a.h"
int main(void){
// method 2:在头文件中声明exter所需变量
// method 1: 解除下面的注释.
// extern int num;
int a = num;
(a);
helloreturn 0;
}
method-2
a.h
void hello(int num);
a.c
#include <stdio.h>
int num = 100;
void hello(int num){
("%d\n",num);
printf}
main.c
#include "a.h"
int main(void){
extern int num;
int a = num;
(a);
helloreturn 0;
}
union
将变量类型组合, 公用一个结构,个人感觉有点像是一个大的结构体(但是其所占用的大小是最大的那个变量的大小),将所有类型进行组合,大大提升代码的可阅读性和可维护性.
#include <stdio.h>
union data{
long age;
char ch;
};
int main(int argc, char* argv[])
{
union data stu1;
.age = 18;
stu1.ch = 'a';
stu1("union.age:%ld,sizeof:%ld\n",stu1.age,sizeof(stu1));
printf("union.ch:%d,sizeof:%ld\n",stu1.ch,sizeof(stu1));
printfreturn 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[])
{
.a = 0b01111111;
ctype.b = 0b10000011;
ctype("%d %d size:%ld\n",ctype.a,ctype.b,sizeof(ctype));
printfreturn 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 && ./test
p4 - NEMU框架选讲(1): 编译运行
总结: - unix 的优劣 - git的使用 一本说unix优劣书:https://web.mit.edu/~simsong/www/ugh.pdf
unix的关键字作为参数的问题
例如
rm -rf xxxx
如果该命令删除一个名为-rf
的文件该如何解决. 为了解决这个问题, linux的命令中出现了-- xxx
- 尽管unix有些问题, 但是还是给我们带来了更多的便利
-rf -- -rf // 后续在linux上无法创建-开头的文件 rm
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
将代码提交到了仓库, 表示作为一个版本.
交互模式
{reset,checkout,add} -p // y(es), n(o), a(ll), q(quit), git
git diff
>### git commit git会使用与当前提交相同的父节点进行一次新提交,旧的提交会被替换。
--amend
git commit --allow-empty git commit
git 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 -j8
basename
获取当前文件名(获取路径的最后一个参数)
basename $(pwd)
vi内置的bash
使用方法
!
:!echo hello
make -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-x86-interpreter
│ ├── obj
│ │ ├── device.d
│ │ │ ├── alarm.o
│ │ │ ├── alarm.d
│ │ │ ├── audio.o
│ │ │ ├── audio.d
│ │ │ ├── device.o
│ │ │ ├── device.d
│ │ │ ├── intr.o
│ │ │ ├── intr
│ │ │ ├── io.d
│ │ │ │ ├── map.o
│ │ │ │ ├── map.d
│ │ │ │ ├── mmio.o
│ │ │ │ ├── mmio-io.d
│ │ │ │ ├── port-io.o
│ │ │ │ └── port.d
│ │ │ ├── keyboard.o
│ │ │ ├── keyboard.d
│ │ │ ├── serial....
查找
.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函数位置
. -name "*.c" -o -name "*.h"| xargs grep --color -nse '\<main\>' find
> ### 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
(argc, argv); init_monitor
随后进入到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:
("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");
printf(0);
exit}
}
}
make run
第一次运行大多会遇到
x86-nemu-interpreter: src/isa/x86/reg.c:20: reg_test: Assertion reg_w(i) == (sample[i] & 0xffff)' failed.
解决方法: 查看报错位置代码