哈工大2022年春季学期计算机系统大作业——程序人生

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业  人工智能未来技术                      

学     号  7203610716                      

班   级  20WJ102                      

学       生  孙铭蔚                

指 导 教 师  刘宏伟                   

计算机科学与技术学院

2021年5月

摘  要

本文以一个简单的程序hello.c为例,分析了程序执行的过程,包括预处理、编译、汇编、链接、进程管理、存储管理等过程,对《深入理解计算机系统》(第三版)中的相关知识进行简要概括。

关键词:预处理、编译 、汇编、链接、进程、存储                          

目  录

第1章 概述 - 4 -

1.1 Hello简介 - 4 -

1.2 环境与工具 - 4 -

1.3 中间结果 - 4 -

1.4 本章小结 - 4 -

第2章 预处理 - 5 -

2.1 预处理的概念与作用 - 5 -

2.2在Ubuntu下预处理的命令 - 5 -

2.3 Hello的预处理结果解析 - 5 -

2.4 本章小结 - 5 -

第3章 编译 - 6 -

3.1 编译的概念与作用 - 6 -

3.2 在Ubuntu下编译的命令 - 6 -

3.3 Hello的编译结果解析 - 6 -

3.4 本章小结 - 6 -

第4章 汇编 - 7 -

4.1 汇编的概念与作用 - 7 -

4.2 在Ubuntu下汇编的命令 - 7 -

4.3 可重定位目标elf格式 - 7 -

4.4 Hello.o的结果解析 - 7 -

4.5 本章小结 - 7 -

第5章 链接 - 8 -

5.1 链接的概念与作用 - 8 -

5.2 在Ubuntu下链接的命令 - 8 -

5.3 可执行目标文件hello的格式 - 8 -

5.4 hello的虚拟地址空间 - 8 -

5.5 链接的重定位过程分析 - 8 -

5.6 hello的执行流程 - 8 -

5.7 Hello的动态链接分析 - 8 -

5.8 本章小结 - 9 -

第6章 hello进程管理 - 10 -

6.1 进程的概念与作用 - 10 -

6.2 简述壳Shell-bash的作用与处理流程 - 10 -

6.3 Hello的fork进程创建过程 - 10 -

6.4 Hello的execve过程 - 10 -

6.5 Hello的进程执行 - 10 -

6.6 hello的异常与信号处理 - 10 -

6.7本章小结 - 10 -

第7章 hello的存储管理 - 11 -

7.1 hello的存储器地址空间 - 11 -

7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -

7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -

7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -

7.5 三级Cache支持下的物理内存访问 - 11 -

7.6 hello进程fork时的内存映射 - 11 -

7.7 hello进程execve时的内存映射 - 11 -

7.8 缺页故障与缺页中断处理 - 11 -

7.9动态存储分配管理 - 11 -

7.10本章小结 - 12 -

第8章 hello的IO管理 - 13 -

8.1 Linux的IO设备管理方法 - 13 -

8.2 简述Unix IO接口及其函数 - 13 -

8.3 printf的实现分析 - 13 -

8.4 getchar的实现分析 - 13 -

8.5本章小结 - 13 -

结论 - 14 -

附件 - 15 -

参考文献 - 16 -


第1章 概述

1.1 Hello简介

hello程序起源于一个能被人类读懂的高级C语言程序。当我们刚学习一门编程语言后,作为程序员的共识,我们总是尝试着在IDE中打印出”hello!”,然而,当我们没有学习计算机底层时,会对这产生很浓厚的好奇心。事实上,我们编写的每一条C语言程序都必须先被转化为一系列的机器语言。下面,我们一起看看编写的hello程序是如何被打印出来的。

Hello的P2P过程:在我们的电脑中,源文件到目标文件的转化分为四个步骤:预处理、编译、汇编和链接。其中分别需要编译器驱动程序调用语言预处理器、编译器、汇编器和链接器。当我们在IDE中编写完hello程序,就得到了源文件。hello.c文件经过预处理器(cpp)得到修改了的源程序,即一个ASCII码的中间文件(hello.i)。之后,通过编译器(cc1)将这个中间文件翻译成汇编程序(hello.s)。然后汇编器(as)将汇编程序翻译成可重定位目标的程序(二进制)叫做hello.o。最后由链接器(ld)创建一个可执行目标二进制程序(hello),这个文件可以被加载到内存中。此后,在shell中输入./hello即可命令系统调用fork函数以及execve等使程序变成进程进而运行。这里的p2p是指程序向进程转化的过程。

Hello的020过程:这个过程对应的内存相关操作,首先加载把hello代码以及数据载入内存,shell为子进程映射虚拟内存,在软硬件结合的情况下,运行直至代码运行结束。在程序运行结束或,内核回收hello进程,删除内存中的相关代码以及数据,一切“归零”。

1.2 环境与工具

硬件环境:硬件1概览:

  型号名称: MacBook Air

  型号标识符: MacBookAir10,1

  芯片: Apple M1

  核总数: 8(4性能和4能效)

  内存: 8 GB

  系统固件版本: 7429.81.3

  操作系统加载程序版本: 7429.81.3

  序列号(系统): C02H479LQ6L5

  硬件UUID: B9297F28-81B7-5DB0-8760-89063F63E0E5

  预置UDID: 00008103-001205960108801E

  激活锁状态: 已启用

硬件2概览:

64 位操作系统, 基于 x64 的处理器

AMD Ryzen 7 4800H with Radeon Graphics  2.90 GHz

LAPTOP-CSSCDDGT

软件环境:Windows 10 64位;Virtualbox; Ubuntu 20.04

开发工具:gcc; gdb; objdump

1.3 中间结果

hello.i: ASCII码的中间文件(预处理器产生),用于分析预处理过程。

hello.s: ASCII汇编语言文件(预处理器产生),用于分析编译的过程。

hello.o:可重定位目标程序(汇编器产生),用于分析汇编的过程。

hello:可执行目标文件(链接器产生),用于分析链接的过程。

hello.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。

hellold.txt:hello的反汇编文件,用于分析可执行目标文件hello。

helloelf.txt:hello.o的ELF格式,用于分析可重定位目标文件hello.o。

helloldelf.txt:hello的ELF格式,用于分析可执行目标文件hello。

1.4 本章小结

本章介绍了Hello的P2P、020的整个过程并介绍了实验的环境、工具以及实验的中间结果。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

预编译又称为预处理,作用是代码文本替换。预处理指令是以“#”开头的代码为编译做准备工作。C语言在编译之前会进行一步处理——预处理,例如#include<stdio.h>命令告诉处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。进而得到另外一个C语言程序,同城以.i作为拓展名。预处理包括宏定义、文件包含、条件编译等。大多数预处理器指令属于下面3种类型:

  1. 宏定义:#define 指令定义一个宏,#undef指令删除一个宏定义。
  2. 文件包含:#include指令导致一个指定文件的内容被包含到程序中。
  3. 条件编译:#if,#ifdef,#ifndef,#elif,#else和#dendif指令可以根据编译器可以测试的条件来将一段文本包含到程序中或排除在程序之外。

作用如下:

  1. 将源文件中以”include”格式包含的文件复制到编译的源文件中。
  2. 用实际值替换用“#define”定义的字符串。
  3. 根据“#if”后面的条件决定需要编译的代码。

2.2在Ubuntu下预处理的命令

首先,在乌班图终端输入命令gcc -E hello.c -o hello.i

预处理过程如下:

图2-2-1预处理命令

图2-2-2预处理结果

2.3 Hello的预处理结果解析

经过我们查看预处理生成的hello.i文件,可以发现,主函数中定义的变量没有发生变化,注意到以#include开头的代码发生了很大改变,被替换成头文件中的内容,同时,源文件中的注释也被删除了。说明预处理对我们的源程序进行了文本性质的处理,进而生成hello.i文件。

2.4 本章小结

本小结介绍了预处理的概念给出了相关实例,分析了程序运行过程,给出了详细说明,例如预处理指令、宏定义命令、文件包含以及条件编译等。1、

大致总结为:预处理阶段删除所有的#define,展开所有宏定义;处理条件预编译指令#if、#ifdef、#else;处理#include指令,引入递归头文件;删除注释,添加行号和文件名标识用于编译时产生调试信息;保留#pragma 编译器指令。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译就是把代码转化为汇编指令的过程,汇编指令只是CPU相关的。也就是由hello.i转化为hello.s的过程。编译会对于处理文件进行词法分析(运用一种有限状态机的算法将源代码的字符序列分割成一系列的记号,将记号分成关键字、标识符、字面量、运算符)、语法分析(对语法分析中产生内容进行语法分析,从而产生语法树,以表达式为节点的数)、语义分析(只分析静态语义,如类型之间的赋值转换,而动态语义是运行时的出现的问题,如0做除数)、源代码优化、代码生成、目标代码优化(产生优化后的汇编代码)。

作用如下:

便于计算机“理解”程序,因为高级语言是便于人理解的语言,而机器只能(或者说更容易)理解汇编语言,通常汇编程序比C语言效率更高,而且它们采用兼容的指令体系,今儿提高兼容性。

3.2 在Ubuntu下编译的命令

编译命令为gcc -S hello.i -o hello.s

编译过程如下:

图3-2-1编译命令

hello.s文件内容如下:

.file "hello.c"

.text

.section .rodata

.align 8

.LC0:

.string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201"

.LC1:

.string "Hello %s %s\n"

.text

.globl main

.type main, @function

main:

.LFB6:

.cfi_startproc

endbr64

pushq %rbp

.cfi_def_cfa_offset 16

.cfi_offset 6, -16

movq %rsp, %rbp

.cfi_def_cfa_register 6

subq $32, %rsp

movl %edi, -20(%rbp)

movq %rsi, -32(%rbp)

cmpl $4, -20(%rbp)

je .L2

leaq .LC0(%rip), %rdi

call puts@PLT

movl $1, %edi

call exit@PLT

.L2:

movl $0, -4(%rbp)

jmp .L3

.L4:

movq -32(%rbp), %rax

addq $16, %rax

movq (%rax), %rdx

movq -32(%rbp), %rax

addq $8, %rax

movq (%rax), %rax

movq %rax, %rsi

leaq .LC1(%rip), %rdi

movl $0, %eax

call printf@PLT

movq -32(%rbp), %rax

addq $24, %rax

movq (%rax), %rax

movq %rax, %rdi

call atoi@PLT

movl %eax, %edi

call sleep@PLT

addl $1, -4(%rbp)

.L3:

cmpl $7, -4(%rbp)

jle .L4

call getchar@PLT

movl $0, %eax

leave

.cfi_def_cfa 7, 8

ret

.cfi_endproc

.LFE6:

.size main, .-main

.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"

.section .note.GNU-stack,"",@progbits

.section .note.gnu.property,"a"

.align 8

.long  1f - 0f

.long  4f - 1f

.long  5

0:

.string  "GNU"

1:

.align 8

.long  0xc0000002

.long  3f - 2f

2:

.long  0x3

3:

.align 8

3.3 Hello的编译结果解析

3.1.1数据

1)常量

hello.c文件中的常量为printf函数的参数(字符串),“用法: Hello 学号 姓名 秒数!\n”和“Hello %s %s\n”。

图 3-3-1hello.c源程序

.LC0:

.string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201"

.LC1:

.string "Hello %s %s\n"

对比代码可知,两个printf中的字符串是常量,这两个字符串常量被.LC0和.LC1标识。

2)局部变量

这个源程序中只有一个局部变量,即循环变量int i = 0

在hello.s中,这个局部变量存储在栈上相关代码为:

.L2:

movl $0, -4(%rbp)

jmp .L3

3)函数参数

函数参数有argc和argv,它们被保存在栈上,通过偏移地址进行访问,相关代码如下:

movl %edi, -20(%rbp)

movq %rsi, -32(%rbp)

3.3.2 操作

1)赋值操作

源程序在给局部变量i赋值时,由于i的数据类型是int占4个字节,只用movl指令,将栈指针移动4,下面操作左侧为立即数0.

movl $0, -4(%rbp)

jmp .L3

在赋值操作中,mov后面的字母代表数据类型不同大小。例如,b,w,l,q分别代表1,2,4,8个字节。

 2)算数操作

在for循环中的++操作,每次云环都会给局部变量i自增1,相应的汇编代码为:

addl $1, -4(%rbp)

此外,还有分配栈帧时的加减操作,例如:

subq $32, %rsp

还有改变数组的偏移操作,例如:

addq $16, %rax

3)关系操作和控制转移操作

if循环中的关系操作和控制转移,例如:

cmpl $4, -20(%rbp)

je .L2

对应与源程序的代码为:

    if(argc!=4){

        printf("用法: Hello 学号 姓名 秒数!\n");

        exit(1);

    }

这段汇编代码会检查argc是否为4,进而设置条件码,如果相等则会跳转到.L2位置。

for循环中的关系操作和控制转移,例如:

cmpl $7, -4(%rbp)

jle .L4

这两句汇编代码对应于源程序:

    for(i=0;i<8;i++){

        printf("Hello %s %s\n",argv[1],argv[2]);

        sleep(atoi(argv[3]));

每次循环结束判断循环条件,进而决定是否跳出循环。

4)数组、指针、结构操作

数组操作:

在主函数中有指针数组char* argv[]

在argv数组中,argv[0],argv[1],argv[2],argv[3]分别对应指向输入程序的路径和名称、学号、姓名、休眠秒数。

movq -32(%rbp), %rax

addq $16, %rax

movq (%rax), %rdx

movq -32(%rbp), %rax

addq $8, %rax

movq (%rax), %rax

movq %rax, %rsi

以及:

movq -32(%rbp), %rax

addq $8, %rax

movq (%rax), %rax

movq %rax, %rsi

leaq .LC1(%rip), %rdi

从上述汇编代码可以看出,访问函数参数argv[0],argv[1],argv[2],argv[3]时候的寻址方法是基址-变址的方法,使用寄存器指向数组的首地址,再通过便宜的地址寻找所需变量.

movq %rsi, -32(%rbp)

从以上代码可知,rsi存储着argc的首地址。

5)函数操作

通过以下代码以及源程序可知,主函数调用了exit,puts,getchar,print,atoi,sleep函数。

leaq .LC1(%rip), %rdi

movl $0, %eax

call printf@PLT

movq -32(%rbp), %rax

addq $24, %rax

movq (%rax), %rax

movq %rax, %rdi

call atoi@PLT

movl %eax, %edi

call sleep@PLT

addl $1, -4(%rbp)

现在逐个函数进行分析:

main函数:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储,并且返回使用movl $0, %eax,由所学知识可知,rax寄存器存储的是返回值,代表返回0.

printf函数:利用leaq .LC0(%rip)将第一个参数地址传递给%rip,其他参数传递方法类似。使用call puts@PLT调用printf函数。

exit函数:使用movl $1, %edi命令传给参数寄存器,当argc!=4时调用该函数,相关汇编指令为call exit@PLT。

sleep函数:相关汇编指令为 call atoi@PLT以及movl %eax, %edi,将atoi函数的返回结果作为参数传入sleep函数,也即是将%eax中的值复制给%edi。该函数通过汇编指令call sleep@PLT调用。

atoi函数:相关汇编指令为addq $24, %rax、movq (%rax), %rax以及movq %rax, %rdi将argv[3]的字符串地址传入%rdi.通过汇编指令call      atoi@PLT调用。

getchar函数通过汇编指令call  getchar@PLT调用。并返回读取字符的ASCII.

3.4 本章小结

本章首先介绍了编译的过程,接下来给出在乌班图下编译的命令。结合hello程序实例分析说明了编译结果,依次介绍了常量、局部变量、函数参数、赋值操作、函数操作、关系操作和控制转移操作、数组、指针、结构操作函数操作,并逐一分析了用到的函数以及对应的汇编指令。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

汇编的概念:概念:汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。

作用:将汇编代码转变为机器指令,生成目标文件。

4.2 在Ubuntu下汇编的命令

命令:gcc hello.s -c -o hello.o

图4-2-1 汇编命令

4.3 可重定位目标elf格式

4.3.1 ELF

用命令readelf -h hello.o查看

查看后结果如下:

图4-3-1 hello.o中的ELF内容

从上图可以得出以下结论:

hello.o是64位文件;数据的存储形式为补码、小端序存储;文件的类型为REL(可重定位文件),入口点地址为0x0,程序头起点为0,节头表的起始位置为1256;文件共有13节。

4.3.2 头节表

使用命令readelf -S hello.o查看

查看结果如下图所示:

图4-3-2hello.o头节表内容

头节表中包含了很多信息,例如节的类型为之和大小等。具体信息可见上图。

4.3.3 符号表

用命令readelf -s hello.o查看,查看结果如下图所示:

图4-3-3hello.o中符号表内容

可以看到,符号表中存储了很多变量名和函数名,具体内容见上图。

4.3.4 重定位节

用命令readelf -r hello.o查看,查看结果如下图所示:

图4-3-4hello.o中重定位节的内容

从上图可以看到,重定位节中包含了偏移量、信息、类型、符号值和符号名称等信息。本程序需要重定位的信息有:.rodata,puts,exit,printf,atoi,sleep,getchar这些符号同样需要与相应的地址进行重定位。

4.4 Hello.o的结果解析

使用命令objdump -d -r hello.o > a.s将hello.o存入a.s中,便于查看

结果如下所示:

hello.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:

   0: f3 0f 1e fa           endbr64

   4: 55                    push   %rbp

   5: 48 89 e5              mov    %rsp,%rbp

   8: 48 83 ec 20           sub    $0x20,%rsp

   c: 89 7d ec              mov    %edi,-0x14(%rbp)

   f: 48 89 75 e0           mov    %rsi,-0x20(%rbp)

  13: 83 7d ec 04           cmpl   $0x4,-0x14(%rbp)

  17: 74 16                 je     2f <main+0x2f>

  19: 48 8d 3d 00 00 00 00 lea    0x0(%rip),%rdi        # 20 <main+0x20>

1c: R_X86_64_PC32 .rodata-0x4

  20: e8 00 00 00 00        callq  25 <main+0x25>

21: R_X86_64_PLT32 puts-0x4

  25: bf 01 00 00 00        mov    $0x1,%edi

  2a: e8 00 00 00 00        callq  2f <main+0x2f>

2b: R_X86_64_PLT32 exit-0x4

  2f: c7 45 fc 00 00 00 00 movl   $0x0,-0x4(%rbp)

  36: eb 48                 jmp    80 <main+0x80>

  38: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  3c: 48 83 c0 10           add    $0x10,%rax

  40: 48 8b 10              mov    (%rax),%rdx

  43: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  47: 48 83 c0 08           add    $0x8,%rax

  4b: 48 8b 00              mov    (%rax),%rax

  4e: 48 89 c6              mov    %rax,%rsi

  51: 48 8d 3d 00 00 00 00 lea    0x0(%rip),%rdi        # 58 <main+0x58>

54: R_X86_64_PC32 .rodata+0x22

  58: b8 00 00 00 00        mov    $0x0,%eax

  5d: e8 00 00 00 00        callq  62 <main+0x62>

5e: R_X86_64_PLT32 printf-0x4

  62: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  66: 48 83 c0 18           add    $0x18,%rax

  6a: 48 8b 00              mov    (%rax),%rax

  6d: 48 89 c7              mov    %rax,%rdi

  70: e8 00 00 00 00        callq  75 <main+0x75>

71: R_X86_64_PLT32 atoi-0x4

  75: 89 c7                 mov    %eax,%edi

  77: e8 00 00 00 00        callq  7c <main+0x7c>

78: R_X86_64_PLT32 sleep-0x4

  7c: 83 45 fc 01           addl   $0x1,-0x4(%rbp)

  80: 83 7d fc 07           cmpl   $0x7,-0x4(%rbp)

  84: 7e b2                 jle    38 <main+0x38>

  86: e8 00 00 00 00        callq  8b <main+0x8b>

87: R_X86_64_PLT32 getchar-0x4

  8b: b8 00 00 00 00        mov    $0x0,%eax

  90: c9                    leaveq

  91: c3                    retq   

分析hello.o的反汇编,并与第3章的 hello.s进行对照分析如下:

经过比较,可以知道,hello.o的反汇编和hello.s大体上比较相似。

有区别的地方如下:

  1. 操作数表示:hello.o反汇编代码中的操作数是以16进制表示,而hello.s中的操作数是以10进制表示。
  2. 分支转移:hello.o反汇编文件的分支转移是通过给出地址进行跳转实现的;ernhello.s文件中的分支转移是通过使用段名称实现的。
  3. 函数调用:hello.s文件中call指令后面的是函数名,而hello.o反汇编文件中call指令后面的是指令的地址。
  4. 全局变量访问:hello.s中全局变量访问是段的名称+寄存器,反汇编文件中全局变量访问是0+寄存器,因为全局变量数据的地址在运行时已经确定,访问也需要重定位。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.5 本章小结

本章介绍了汇编的过程,并且分析了机器语言的构成,与汇编语言的映射关系。

(第4章1分)


第5章 链接

5.1 链接的概念与作用

链接的概念:链接是将一个或多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件。例如,hel1o程序调用了 printf 函数,它是每个C编译器都提供的标淮C库中的一个函数。printf 两数存在于一个名为 printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hel1o.o程序中。链接器(ld)就负责处理这种合并。结果就得到hel10文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。

链接的作用:1)符号解析 2)重定位

5.2 在Ubuntu下链接的命令

链接的命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

图5-2-1 链接命令

5.3 可执行目标文件hello的格式

5.3.1 ELF

在ubuntu终端输入readelf -a hello > all.txt,可以将内容输出到all.txt中,方便查看信息。ELF头信息如下图所示。

图5-3-1hello ELF头内容

从上图可以得出以下结论:

hello的数据的存储形式为补码、小端序存储;文件的类型为ELF64,入口点地址为0x1100,程序头起点为64,节头表的起始位置为14208,表明重定位已经完成;文件共有27节,多于hello.o文件数目。

5.3.2 节头表

图5-3-2hello中头节表部分内容

从上图可以得出以下结论:

hello中头节表包含了很多信息,例如节的类型为之和大小等,hello中节头表的条目数多于hello.o中节头表的条目数。特别地,每一节都有实际的地址而非hello.o中地址全是0。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

5.3.3 符号表

查看符号表结果如下图所示

图5-3-3 hello符号表内容

由上图可知,hello 符号表内容包括很多符号引用和定义的信息,例如目标文件的符号表包含定位和重定位程序的符号定义和符号引用所需的信息。每个符号表是一个条目的数组,每个条目包括value:距定义目标的节的起始位置的偏移;size:目标的大小;type:指明数据还是函数;bind:表示符号是本地的还是全局的等等。

5.3.4动态区域

查看结果如下图所示

图5-3-4动态区域内容

5.4 hello的虚拟地址空间

在乌班图中运行命令edb –-run hello运行hello。

运行结果如下图所示:

图5-4-1edb运行结果图

经过初步分析,程序被载入至地址0x401000~0x402000中。在该地址范围内,每个节的地址都与对应的 Address 相同。根据edb查看的结果,在地址空间0x401000~0x401fff中存放着与地址空间0x401000~0x402000相同的程序,在0x401fff之后存放的是.dynamic到.shstrtab节的内容。

5.5 链接的重定位过程分析

使用命令objdump -d -r hello > hello.txt将hello文件返汇编代码存储到hello.txt便于查看。

 

图5-5-1获取反汇编代码命令

对hello进行反汇编部分结果如下:

图5-5-1 hello的反汇编部分结果

hello和hello.o相比,有很多地方发生变化。

首先多了很多经过重定位之后的函数,如多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt,atoi@plt等,hello.o在.text段之后只有一个main函数。相关事例如下:

 

图5-5-2相关示例

其次,跳转指令参数发生变化。在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。相关例子如下所示:

图5-5-3相关示例

最后,hello.o的地址是从0开始的,是相对地址,而hello的地址是从0x401000(_init的地址)开始的,是已经进行重定位之后的虚拟地址;在hello的main函数中,条件跳转指令和call指令后均为绝对地址,而hello.o中是相对于main函数的相对地址。相关例子如下所示:

图5-5-4相关示例

5.6 hello的执行流程

以下格式自行编排,编辑时删除

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

_dl_start、_dl_init

_start

_libc_start_main

_main

_printf

_exit

_sleep_getchar

_dl_runtime_resolve_xsave

_dl_fixup

_dl_lookup_symbol_x

Exit

ld-2.27.so!_dl_start

ld-2.27.so!_dl_init

hello!_start

lib-2.27.so!__libc_start_main

hello!puts@plt

5.7 Hello的动态链接分析

   (以下格式自行编排,编辑时删除

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

编译器在运行时会添加重定位记录,等待动态链接器处理,目的是避免运行时修改调用模块的代码段。动态链接器使用过程链接表+全局偏移量表实现函数的动态链接,在全局偏移量表中存放函数目标地址,过程链接表使用全局偏移量表中地址跳转到目标函数,在加载时,动态链接器会重定位全局偏移量表中的每个条目,使得它包含目标的正确的绝对地址。

.got与.plt节保存着全局偏移量表,其内容从地址0x404000开始。通过edb查看,在dl_init调用前,其内容如下:

               exit@plt的GOT条目       调用前的.got.plt

图5-7-1调用前的情况

在调用后,其内容变为:

                                                                           调用后的.got.plt

图5-7-2调用后的情况

比较可以得知,0x404008~0x404017之间的内容,对应着全局偏移量表的内容发生了变化。其一保存的是指向已经加载的共享库的链表地址,其二是动态链接器在ld-linux.so模块中的入口。这样,接下来执行程序的过程中,就可以使用过程链接表和全局偏移量表进行动态链接。

5.8 本章小结

本章介绍了链接相关概念,介绍了乌班图中链接器链接的命令,通过可执行目标文件hello,分析了hello虚拟地址空间,借助edb软件分析了重定位过程以及执行流程。


第6章 hello进程管理

6.1 进程的概念与作用

进程被认为是计算机科学中最深刻、最成功的概念之一。在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。其实,这都是进程的功劳。

进程的经典定义是一个执行中的程序实例。

进程的作用是切换上下文,多进程可以完成多任务,给用户一种假象,就是好像我们的程序独占地使用处理器和操作系统,进而提高程序执行效率。

6.2 简述壳Shell-bash的作用与处理流程

Shell指的是为用户提供UI界面的软件窗口,用户输入命令,它可以对信号进行处理,让用户与内核进行交互操作。

Shell的作用是解释用户的命令。

处理流程:

1.读取用户由键盘输入的命令

2.对命令进行分析并处理

3.建立一个子进程;

4.子进程根据文件名(命令名)到目录中查找有关文件,将他调入内存,并创建新的文本段,并根据写时拷贝的方式创建相应的数据段、堆栈段

5.当子进程完成处理或者出现异常后,通过exit()或_exit()函数向父进程报告

6.终端进程调用wait函数来等待子进程完成,并对子进程进行回收

6.3 Hello的fork进程创建过程

由于hello不是shell内部程序,所以shell会通过创建子进程来完成相关操作。

新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库等等。子进程还获得与父进程任何打开文件描述符相同的副本。

当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的 PID。

fork函数调用一次返回两次一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork 返回子进程的的 PID。在子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辦程序是在父进程还是在子进程中执行。需要注意的是,子进程和父进程是并发进行的。

6.4 Hello的execve过程

创建一个子进程以后,子进程调用exceve函数,这个函数加载并运行可执目标文件,在当前子进程的上下文加载并运行一个新的程序,即hello程序,需要以下步骤:

  1. execve函数在当前进程的上下文中加载并运行一个新程序。
  2. 首先,execve函数加载并运行可执行目标文件filename,且带参数列表 argv 和环境变量列表 envp。只有当出现错误时,例如找不到fi1ename和execve 才会返回到调用程序。所以,与fork一次调用返画两次不同,execve 调用一次并从不返回。
  3. 在 execve 加载了 filename 之后,它调用启动代码。
  4. main 函数有3个参数:a)argc,它给出 argv[]数组中非空指针的数量.b)argv指向 argv[]数组中的第一个条目.c)envp,指向envp[]数组中的第一个条目。当main 开始执行时,用户栈的组织结构如下图所示。

图6-4-1一个程序开始执行时,用户栈典型组织结构

6.5 Hello的进程执行

6.5.1 进程时间片

首先介绍一下时间片,时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。因此,进程时间片就是一个进程执行它的控制流的一部分的每一时间段

6.5.2 进程上下文切换与调度

操作系统内核使用一种称为上下文切换(context switch)的较高层形式的异常控制流来实现多任务。上下文切换机制是建立在前文已经讨论过的那些较低层异常机制之上的。

内核为每个进程维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点奇存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程己打开文件的信息的文件表。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。

上下文切换过程:

1)保存当前进程的上下文

2)恢复某个先前被抢占的进程被保存的上下文

3)将控制传递给这个新恢复的进程

6.5.3 用户态与核心态转换

当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阳塞,那么内核可以让当前进程休眠,切换到另一个进程。比如,如果一个read 系统调用需要访问磁盘,内核可以选择执行上下文切换,运行另外一个进程,而不是等待数据从磁盘到达。

 sleep 系统调用可以显式地请求让调用进程休眠。一般而言,即使系统调用没有阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。

中断也可能引发上下文切换。比如,所有的系统都有某种产生周期性定时器中断的机制,通常为每 1毫秒或每 10毫秒。每次发生定时器中断时,内核就能判定当前进程已经运行了足够长的时间,并切换到一个新的进程。

进程由常驻内存的操作系统代码块(称为内核)管理大内核不是一个单独的进程,而是作为现有进程的一部分运行。通过上下文切换,控制流通从一个进程传递到另一个进程。

图 6-5-1进程切换上下文图解

6.6 hello的异常与信号处理

  1. 正常运行:

图6-6-1正常运行状态截图

程序完成被正常回收。

2)按下ctrl+z

图6-6-2按下ctrl+z后运行状态截图

这时输入命令PS查看进程,如下图所示:

图6-6-3输入ps指令后程序输出

经初步分析,hello进程没有结束,而是被暂时挂起,PID为45856

使用jobs指令查看,如下:

图6-6-4输入jobs指令后程序输出

使用pstree查看hello具体位置,如下:

图6-6-5输入pstrees指令后程序输出

使用fg 1命令调用它到前台运行,如下图所示:

图6-6-6输入fg 1指令后程序输出

使用ps指令再次查看,可以知道,程序先打印了指令,然后继续执行中断的程序,这是互殴程序已经被回收,如下图所示:

图6-6-7输入ps指令后程序输出

3)按下ctrl+c

图6-6-8按下ctrl+c后程序输出

可以看到进程直接被终止,被回收

4)随机输入

在执行的程序中随即输入,可以看到输入被存入stdin中,按下回车键,在程序结束后会输出,但是对程序没有任何影响,如下图所示:

图6-6-9随机输入后程序输出

5)输入kill可以杀死指定进程,只要给出进程pid

6.7本章小结

本章讲述了进程的概念与作用,进程的概念与作用,简述了壳Shell-bash的作用与处理,介绍了hello的fork迸程创建过程以及execve过程,介绍了hello的进程执行以及异常常与信号处理流程。

(第6章1分)


第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:编译器编译程序时,会为程序生成代码段和数据段,然后将所有代码放到代码段中,将所有数据放到数据段中。最后程序中的每句代码和每条数据都会有自己的逻辑地址。

线性地址:CPU加载程序后,会为这个程序分配内存,所分配内存又分为代码段内存和数据段内存。代码段内存的基址保存在CS中,数据段内存的基址保存在DS中。

虚拟地址:虚拟内存中的地址

物理地址:真实物理内存中的地址

7.2 Intel逻辑地址到线性地址的变换-段式管理

段式管理的思想是吧程序按照不同的内容分成不同的段每个段有自己的名字每个段是一个首地址为零,连续的一维的线性空间。根据需要段可以动态增长。对段式虚地址空间访问包括两个部分:段名和段内地址。在这种管理模式中以段为单位分配内存单元然后通过地址映射机构把段式虚拟地址转换成实际的内存物理地址。逻辑地址由段号S与段内偏移量W两部分组成。每个段分配为一个连续的内存区域

7.3 Hello的线性地址到物理地址的变换-页式管理

虚拟地址包含两个部分虚拟页号虚拟页面偏移被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。将PTE中的PPN与VPO串联,得到最终的物理地址。

图7-3-1虚拟地址中用以访问TLB的组成部分

虚拟内存被分割为虚拟页,理内存也被分割成物理页。利用页表来管理虚拟页,页表就是一个页表条目的数组,每个PTE由一个有效位和一个n位地址字段组成,有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。如果发生缺页,则从磁盘读取。

图7-3-2页式管理

7.4 TLB与四级页表支持下的VA到PA的变换

一到三级页表中存放的数据是指向下一级页表的首地址,而不是物理页号。逐步访问到第四级页表,第四级页表中装的就是物理页号,通过第四级页表读出的物理页号链接上虚拟地址中的VPO获得物理地址。以下为k级页表

图7-4-1k级页表

我们可以发现,36位VPN被划分成了4个9位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推。

7.5 三级Cache支持下的物理内存访问

当我们获取了物理地址PA后,PA分为CT(高速缓存标记),CI(高速缓存索引),CO(缓冲块内的字节偏移量)三部分。首先对于当前PA根据CI在L1cache查找索引然后根据高速缓存标记CT判断是否在组内存在该块,若存在该块且标记位为1则命中,再根据CO获取块偏移后结果,将该结果返回给CPU。若不命中则按照相同的查找策略按L2cache、L3cache、主存这样以此向下查找下去,找到相应块后,若上一层组内缓存有空闲块则将它写入空闲块中,否则则采用相应策略选取牺牲块用找到的块将其替换,替换到L1后返回结果。

7.6 hello进程fork时的内存映射

执行fork()函数时,内核会为新进程分配pid创建新进程的虚拟内存,新进程会拥有当前进程mm_struct,区域结构,页表的副本。但是两进程页面只读,区域写时复制。

当fork()函数在从新的进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同但相互独立,映射的也是同一个物理内存。当这两个进程中的任一个进行写操作时,写时复制机制就会创建新页面,在新的页面中进行写操作,并且原来的虚拟内存映射到创建的新页面上综上所述可以知道每个进程都具有自己的地址空间。

7.7 hello进程execve时的内存映射

execve函数在shell中加载并运行包含在可执行文件hello中的程序,用hello程序有效地替代了当前程序。加载hello进程的主要步骤如下:

  1. 首先删除已存在的用户区域,也就是将shell与hello都有的区域结构删除。然后映射私有区域,即为新程序的代码、数据、bss和栈区域创建新的区域结构,均为私有的、写时复制的
  2. 映射共享区域,将一些动态链接库映射到hello的虚拟地址空间
  3. 设置程序计数器,使之指向使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理

在盛拟内存的习惯说法中,DRAM 缓存不命中称为缺页(page faulo)。有效缺页故障是一种常见的故障,当指令引用一个虚拟地址,在 MMU 中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。结下来,我们以一个例子介绍故障处理流程。

CPU 引用了VP3中的一个字VP3并未缓存在DRAM 中。地址翻译硬件从内存中读取 PTE 3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个辆牲页,在此例中就是存放在 PP3中的VP4。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再绥存在主存中。

图7-8-1缺页事例

接下来,内核从磁盘复制 VP 了到内存中的 PP3,更新PTE 3,随后返回当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP了已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。下图展示了在缺页之后我们的示例页表的状态。

图7-8-2缺页后的处理

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

当我们的程序需要额外空间时我们就要使用动态内存分配。虽然可以使用低级的mmap 和munmap 西数来创建和删除虛拟内存的区域,但是C程序员还是会觉得当运行时需要额外虚拟内存时,用动态内存分配器(dynamic memory allo.cator)更方便,也有更好的可移植性。

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)(见图 )。 

                                                

7-9-1

系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break”),它指向堆的顶部。分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虛拟内存片(chunk),要么是已分配的,要么是空闲的。己分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在手由哪个实体来负责释放已分配的块。接下来我们着重介绍显式分配器。C标准库提供了一个称为mal1oc 程序包的显武分配器。程序通过调用mal1oc 函数来从堆中分配块。

图7-9-2 malloc函数

mal1oc 函数返回一个指针,指向大小为至少 size宇节的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。实际中,对齐依赖于编译代码在 32 位模式(gcc-m32)还是64位模式(默认的)中运行。在32位模式中,mal1oc 返回的块的地址总是8的倍数。在64位模式中,该地址总是16的倍数。

当程序不再需要malloc分配的区域时,需要通过free函数进行释放:

图7-9-3free函数

接下来,我们看一下介绍一下动态内存管理的策略。

程序使用动态内存分配最重要的原因是:有时,当我们运行程序时,才会直到某些数据结构的大小,这样便需要使用到动态内存分配。例如,要求一个C语言程序读一个ASCII码整数的链表,每一行一个整数,从stdin到一个C数组。输入是由整数n和接下来要读和存储到数组中的n个整数组成的。最简单的方法就是静态地定义这个数组,最大数组大小MAXN固定。但是硬编码数组界限的出现对于拥有百万行代码和大量使用者的大型软件产品而言,会变成一场维护的噩梦一种更好的方法是在运行时,在已知了,的值之后,动态地分配这个数组。使用这种方法,数组大小的最大值就只由可用的虚拟内存数量来限制了。hello中的printf作为一个已经编译、汇编好了,等待链接的函数,修改固定参数也是不现实的。我们可以使用类似的方法进行限制。

以上就是动态内存管理的策略。

7.10本章小结

本章介绍hello的存储器地址空间。结合了hello,介绍了逻辑地址、线性地址、虚拟地址、物理地址的概念,以及它们的区别与联系,互相转化的方法。分析了段式管理的大致流程接着我们分析了页式管理是如何完成线性地址到物理地址的变换的。

我们分析了TLB与四级页表支持下的VA到PA的变换。高速地址变址缓存TLB有加速对于页表访问的功能。以四级页表为例,介绍了多级页表的层次、工作流程以及节省空间的优点。同时介绍了三级Cache支持下的物理内存访问的流程,包括在命中情况下的与未命中情况下的。

分析了hello进程fork与execve时的内存映射介绍了缺页故障与缺页中断的处理。以一个VM缺页为例,介绍了缺页中断的处理流程。最后我们分析了动态存储分配管理从动态内存管理的基本方法与动态内存管理的策略两个方面对动态内存管理进行介绍。

(第7章 2分)


第8章 hello的IO管理

8.1 Linux的IO设备管理方法

以下格式自行编排,编辑时删除

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

以下格式自行编排,编辑时删除

8.3 printf的实现分析

以下格式自行编排,编辑时删除

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

(以下格式自行编排,编辑时删除)

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

以下格式自行编排,编辑时删除

(第8章1分)

结论

当我们在IDE中编写完hello程序,就得到了源文件。hello.c文件经过预处理器(cpp)得到修改了的源程序,即一个ASCII码的中间文件(hello.i)。之后,通过编译器(cc1)将这个中间文件翻译成汇编程序(hello.s)。然后汇编器(as)将汇编程序翻译成可重定位目标的程序(二进制)叫做hello.o。最后由链接器(ld)创建一个可执行目标二进制程序(hello),这个文件可以被加载到内存中。此后,在shell中输入./hello即可命令系统调用fork函数以及execve等使程序变成进程进而运行。

在内存管理方面,加载器把hello代码以及数据载入内存,shell为子进程映射虚拟内存,在软硬件结合的情况下,运行直至代码运行结束。在程序运行结束或,内核回收hello进程,删除内存中的相关代码以及数据,一切“归零”。

至此hello一生结束,计算机系统的课程也告一段落。

我们都知道,学习是永无止境的,当我最初看到这本教材时,很难想象有一天我会把它学完,今天我做到了。有人曾说,书是越读越薄的,我认为书是越读越厚的,在阅读CSAPP全书时,我还要到网上去查阅相关资料,让我了解了书本之外的知识,真的让我大开眼界。回顾在学习这本书的经历,有遗憾,也有感动。感动的是有尽心尽力的老师和助教们为我们答疑,遗憾是我没有把书中习题全做完,在进货的日子里,我还会继续努力学习计算机相关知识,争取真正做到理解计算机系统,理解高人们的智慧思想。

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

(结论0分,缺失 -1分,根据内容酌情加分)


附件

hello.i: ASCII码的中间文件(预处理器产生),用于分析预处理过程。

hello.s: ASCII汇编语言文件(预处理器产生),用于分析编译的过程。

hello.o:可重定位目标程序(汇编器产生),用于分析汇编的过程。

hello:可执行目标文件(链接器产生),用于分析链接的过程。

hello.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。

all.txt:hello的反汇编文件,用于分析可执行目标文件hello。

(附件0分,缺失 -1分)


参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

(参考文献0分,缺失 -1分)

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蔚来可期^=*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值