精华内容
下载资源
问答
  • 你一定要知道的计算机底层知识
    千次阅读
    2020-05-06 21:17:37

    前言

    虽然在程序员的职业生涯中,计算机底层知识可能很少直接涉及,但并不意味着这部分知识不重要。

    对于计算机底层实现的深入了解,能帮助你了解计算机的运行原理,更好地设计高效的架构,并且有助于调试、判断错误。特别地,对于多线程的理解尤为重要:现今的程序架构都需要并发处理,如何协调不同线程之间的分工协作,避免死锁、同步出错等问题,是程序员应当具备的技能。对于后端工程师而言,良好的操作系统基础知识更是深刻理解并实现复杂分布式系统的前提条件。

    进程 vs.线程


    进程( process)与线程( thread)最大的区别是:进程拥有自己的地址空间,某进程内的线程对于其他进程不可见,即进程 A 不能通过传地址的方式直接读写进程 B 的存储区域。进程之间的通信需要通过进程间通信( Inter-Process Communication, IPC)。与之相对的,同一进程的各线程间之间可以直接通过传递地址或全局变量的方式传递信息。

    此外,进程作为操作系统中拥有资源和独立调度的基本单位,可以拥有多个线程。通常操作系统中运行的一个程序就对应一个进程。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。相比进程切换,线程切换的开销要小很多。线程于进程相互结合能够提高系统的运行效率。

    线程可以分为两类:

    一类是用户级线程( user level thread)。对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程称为主线程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。 用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。

    另一类是内核级线程( kernel level thread)。对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的 CPU,以实现真正的并行计算。

    事实上,在现代操作系统中,往往使用组合方式实现多线程,即线程创建完全在用户空间中完成,并且一个应用程序中的多个用户级线程被映射到一些内核级线程上,相当于是一种折中方案。


    上下文切换

    对于单核单线程 CPU 而言,在某一时刻只能执行一条 CPU 指令。上下文切换( Context Switch)是一种将 CPU 资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。


    系统调用

    系统调用( System Call)是程序向系统内核请求服务的方式。可以包括硬件相关的服务(例如,访问硬盘等),或者创建新进程,
    调度其他进程等。系统调用是程序和操作系统之间的重要接口。

     

    Semaphore/Mutex

    当用户创立多个线程/进程时,如果不同线程/进程同时读写相同的内容,则可能造成读写错误,或者数据不一致。此时,需要通过加
    锁的方式,控制核心区域( critical section)的访问权限。对于semaphore 而言,在初始化变量的时候可以控制允许多少个线程/进
    程同时访问一个核心区域,其他的线程/进程会被堵塞,直到有人解锁。 Mutex 相当于只允许一个线程/进程访问的 semaphore。此外,根据实际需要,人们还实现了一种读写锁( read-write lock),它允许同时存在多个读取者( reader),但任何时候至多只有一个写入者( writer),且不能与读取者共存。

     

    死锁

    在引入锁的同时,我们遇到了一个新的问题:死锁( Deadlock)。

    死锁是指两个或多个线程/进程之间相互阻塞,以至于任何一个都不能继续运行,因此也不能解锁其他线程/进程。例如,线程 A 占有 lockA,并且尝试获取 lock B;而线程 2 占有 lock B,尝试获取 lock A。此时,两者相互阻塞,都无法继续运行。产生死锁的 4 个条件概括如下(只有当 4 个条件同时满足时才会产生死锁):

    1. 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
    2. 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
    3. 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
    4. 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系

     

    生产者消费者


    生产者消费者模型是一种常见的通信模型:生产者和消费者共享一个数据管道,生产者将数据写入 buffer,消费者从另一头读取数
    据。对于数据管道,需要考虑为空和溢出的情况。同时,通常还需要将这部分共享内存用 mutex 加锁。在只有一个生产者一个消费者的情况下,可以设计无锁队列( lockless queue),线程安全地直接读写数据。

     

    进程间通信
     

    在介绍进程的时候,我们提起过一个进程不能直接读写另一个进程的数据,两者之间的通信需要通过进程间通信进行。进程通信的方
    式通常遵从生产者消费者模型,需要实现数据交换和同步两大功能。
    ( 1)共享内存( Shared-memory) + semaphore
           不同进程通过读写操作系统中特殊的共享内存进行数据交换,进程之间用 semaphore 实现同步。
    ( 2)信息传递( Message passing)
          进程在操作系统内部注册一个端口,并且监测有没有数据,其他进程直接写数据到该端口。该通信方式更加接近于网络通信方式。事实上, 网络通信也是一种 IPC,只是进程分布在不同机器上而已。


    逻辑地址/物理地址/虚拟内存
     

    所谓的逻辑地址,是指计算机用户(例如程序开发者)看到的地址。例如,当创建一个长度为 100 的整型数组时,操作系统返回一个
    逻辑上的连续空间:指针指向数组第一个元素的内存地址。由于整型元素的大小为 4 个字节,故第二个元素的地址时起始地址加 4,以此类推。事实上,逻辑地址并不一定是元素存储的真实地址,即数组元素的物理地址(在内存条中所处的位置),物理地址并不是连续的,只不过操作系统通过地址映射,将逻辑地址映射成连续的,这样更符合人们的直观思维。

    另一个重要概念是虚拟内存。操作系统读写内存的速度可以比读写磁盘的速度快几个量级。但是,内存价格也相对较高,不能大规模
    扩展。于是,操作系统可以将部分不太常用的数据移出内存,存放到价格相对较低的磁盘缓存,以实现内存扩展。操作系统还可以通过算法预测哪部分存储到磁盘缓存的数据需要进行读写,提前把这部分数据读回内存。虚拟内存空间相对磁盘而言要小很多,因此,即使搜索虚拟内存空间也比直接搜索磁盘要快。唯一慢于磁盘的可能是,内存、虚拟内存中都没有所需要的数据,最终还需要从硬盘中直接读取。这就是为什么内存和虚拟内存中需要存储会被重复读写的数据,否则就
    失去了缓存的意义。

    现 代 计 算 机 中 有 一 个 专 门 的 转 译 缓 冲 区 ( Translation Lookaside Buffer, TLB),用来实现虚拟地址到物理地址的快速转换。与内存/虚拟内存相关的还有以下两个概念:

          ( 1) Resident Set
           当一个进程在运行的时候,操作系统不会一次性加载进程的所有数据到内存,只会加载一部分正在用,以及预期要用的数据。其他数据可能存储在虚拟内存,交换区和硬盘文件系统上。被加载到内存的部分就是 resident set。

          ( 2) Thrashing
           由于 resident set 包含预期要用的数据,理想情况下,进程运行过程中用到的数据都会逐步加载进 resident set。但事实往往并非如此:每当需要的内存页面( page)不在 resident set 中时,操作系统必须从虚拟内存或硬盘中读数据,这个过程被称为内存页面错误( page faults)。当操作系统需要花费大量时间去处理页面错误的情况就是 thrashing。
     

    文件系统

    UNIX 风格的文件系统利用树形结构管理文件。每个节点有多个指针,指向下一层节点或者文件的磁盘存储位置。文件节点还附有文件的操作信息( metadata),包括修改时间、访问权限等。用户的访问权限通过访问控制表( Access Control List)和能力表( Capability List)实现。前者从文件角度出发,标注了每个用户可以对该文件进行何种操作。后者从用户角度出发,标注了某用
    户可以以什么权限操作哪些文件。

    UNIX 的文件权限分为读、写和执行,用户组分为文件拥有者、组和所有用户。可以通过命令对三组用户分别设置权限。

     

    实时 vs.分时操作系统

    操作系统可以分为实时操作系统( Real-Time System),和分时操作系统( Sharing Time System)。通常计算机采用的是分时,即多个进程/用户之间共享 CPU,从形势上实现多任务。各个用户/进程之间的调度并非精准度特别高,如果一个进程被锁住,可以给它分配更多的时间。而实时操作系统则不同,软件和硬件必须遵从严格的deadline,超过时限的进程可能直接被终止。在这样的操作系统中,每次加锁都需要仔细考虑。

     

    编译器

    对于高级语言来说,代码需要通过编译才能够运行。编译通过编译器( Compiler)实现,是一个将程序源代码转换成二进制机器码的
    过程。计算机可以直接执行二进制代码。在编译的过程中,编译器需要进行词法分析( Lexical Analysis)、解析( Parsing)和过渡代码生成( Intermediate Code Generation)。编译器的好坏可以直接影响最终代码的执行效率。
     

    更多相关内容
  • 计算机底层数据

    2012-11-02 10:53:17
    计算机底层数据,读取各类数据,内存,io,pci等
  • 计算机底层原理——汇编语言

    万次阅读 多人点赞 2020-08-19 12:51:57
    想要成为高级程序员,我们必须要学会汇编语言,汇编语言是非常重要的计算机底层技术,一般用于底层的编写。不懂汇编的程序员算不上一个好的程序员,充其量是一个熟练使用某种语言的工程师,而编程高手一定要研究底层...

    前言

    备注:该技术博客的内容是我根据技术视频整理与总结的(并非复制粘贴)。原视频源于【遇见狂神说】

    如果我们想要做高级程序员,汇编语言是我们必经之路,汇编让我们跳出传统的编程思想,往底层学习,对我的技术提升非常非常重要。总而言之,想要成为高级程序员,我们必须要学会汇编语言,汇编语言是非常重要的计算机底层技术,一般用于底层的编写。不懂汇编的程序员算不上一个好的程序员,充其量是一个熟练使用某种语言的工程师,而编程高手一定要研究底层

    1.机器语言

    何为语言,就是人和人之间交流的工具。而汇编语言就是计算机的语言。

    机器语言(二进制):

    主流的电子计算机使用二进制,计算机只认识 0和1,因为在电路中只有两种状态,要么通电要么断电,我们用数字表示这两种状态就是0和1,我们可以用0和1与计算机交流。

    机器语言就是由0和1构成的语言,我们很难理解,几乎看不懂。而我们需要将这些复杂的机器语言(一堆0和1的数字)简化,就需要助计符(INC DEC MUL DIV等),也就是汇编语言。

    我们掌握了汇编语言就可以操作计算机的底层,深入一点就是可以直接操作计算机里面的 位。
    汇编语言助记机器语言,所以说我们学会了汇编语言就学会了机器语言。

    学习汇编就是为了理解计算机怎么操作,每一行代码怎么被计算机执行,这些原理非常重要!

    在这里插入图片描述
    在这里插入图片描述

    2.进制思想本质

    学习进制的障碍:
    很多人搞不懂进制的主要原因是因为我们从小接受了十进制的概念,逢十进一的原则深入人心。
    人本能的选择就是 十进制。

    常见进制:
    1进制:逢一进一,1 1
    2进制:逢二进一,计算机进制
    8进制:逢八进一,8个符号组成:0 1 2 3 4 5 6 7
    10进制:逢十进一,10个符号组成:0 1 2 3 4 5 6 7 8 9
    16进制: 逢十六进一,16个符号组成:0 1 2 3 4 5 6 7 8 9 a b c d e f

    进制其实非常简单,只需要会 查数

    测试:
    在这里插入图片描述
    进制可以自定义:
    小朱同学的十进制:0 2 4 7 8 a b r d f,可以随便定义。
    由此我可以使用进制加密,防止被爆破(暴力破解)。

    3.二进制

    目前的计算机(电子计算机)使用二进制 0 1

    现在的计算机(电子计算机):
    都是通过集成电路来实现电路的有电和无电,高电平和低电平,表现出来两个值0 1。

    在这里插入图片描述
    由于二进制写起来非常麻烦,我们就需要简写二进制,所以我们就去写16进制。

    在这里插入图片描述

    拓展:
    未来的量子计算机:(传道)
    可以实现量子计算的机器。

    量子计算机的 单位:昆比特。也就是所谓的(量子比特),量子的两态(光子,磁场)来表示。
    量子的计算速度远远超越了现在的电子计算速度。
    光子:正交偏振方向。
    磁场:电子的自旋方向。

    如今我们已经到了21世纪,计算力已经快到尽头了,没办法突破(落伍)的本质问题!
    我们想要突破这个本质问题,就要使用量子计算机。核心要素就是提高计算机的计算力!

    2019年,Google研究人员展示其最新54比特量子计算机,该计算机只用200秒便可计算完毕当前世界最大的超级计算机需1万年进行的运算。
    2020年.6.18,霍尼韦尔公司推出了量子体积64的量子计算机
    霍尼韦尔还表示,将在一年之内得到至少10个有效量子比特,相当于1024个量子体积。
    如果可以量产,大规模普及到民用之后,我们这些程序员是第一批使用他们的人。因为我们要
    针对量子计算机写程序。

    我们为什么学习理解二进制?
    如果我们了解寄存器,内存,位的概念,计算机底层的每一个位都是有含义的(汇编入门理解的基础)。每一个0和1都代表一种状态,计算机会按照这种状态去执行特定的功能。程序运行时候会快速发生变化,每一个变化就会产生不同的状态,就比如:移动鼠标为什么会动,这底层如何实现非常的复杂。

    4.数据宽度

    计算机的内存是有限制的,内存不可能是无穷大的。所以我们就需要给数据增加数据宽度。

    在计算计领域,我们要记住:所有的内存,所有的操作,都要给数据加上宽度,尤其是C,C++,Java这种强类型语言,都需要定义数据类型!为什么要需要定义类型?因为计算机底层需要我们这些数据定义宽度。

    有了宽度,就有了一些基本的。常用量包含字节,字,双字等…

    位 (bit):0 1
    字节 (byte):(8位) 0-0xFF
    字 (word):0-0xFFFF
    双字 (dword):0-0xFFFFFFF

    在计算机中,每一个数据都需要给它定义类型。定义类型的原因就是给它定义宽度。在内存中的宽度。

    5.有符号数和无符号数

    计算机它并不知道我们写的数字是正还是负。我们可以通过正负号来判断,而计算机如何去表示正负呢?我们接下来了解一下!

    数据都是有宽度的。 那么每个数据代表什么意思呢?

    规则
    就好比我们解析一个音频:比如说为什么网易云可以放出MP3?那是因为有个MP3的编码格式,我们根据这个格式解码才能放出声音对应的格式。如果是一个视频就要遵守视频的解码规则…

    现在我们需要给二进制解码增加一个规则。
    1.无符号数规则:
    你这个数字是什么,那就是什么,无关正负。

    1 0 0 1 1 0 1 0  转换十六进制为: 0x9A   十进制为:154
    

    2.有符号数规则:
    最高位是符号位。 如果最高位是1,就代表一个负数。如果最高位是0,就代表是一个正数。

    1 0 0 1 1 0 1 0  如何转换十六进制?
    

    这里就涉及一套计算机规则:就是原码反码补码。

    6.原码反码补码

    为什么学原码反码补码?
    因为我们之后要用它来计算。

    编码规则:(无符号数编码规则没什么可说的,写的数字是什么就是什么)

    有符号数编码规则有三种状态:**原码,反码,补码。**我们来依此学习一下这三种状态。

    1.原码: 最高位是符号位,对齐它的位进行本身的绝对值即可。
    2.反码: 分为正数和负数

    • 正数:反码和原码相同。
    • 负数:符号位一定是1,其余位对原码取反。

    3.补码:

    • 正数:补码和原码相同
    • 负数:符号一定是1,对反码进行+1

    举个例子:

    现在我说的这些都是 8 位 
    
    如果是正数,都是一样的。
    对1取值:
    原码:0 0 0 0  0 0 0 1
    反码:0 0 0 0  0 0 0 1
    补码:0 0 0 0  0 0 0 1
    
    如果是负数
    对-1取值:
    原码:1 0 0 0  0 0 0 1
    反码:1 1 1 1  1 1 1 0
    补码:1 1 1 1  1 1 1 1-7取值:
    原码:1 0 0 0  0 1 1 1
    反码:1 1 1 1  1 0 0 0
    补码:1 1 1 1  1 0 0 13+5取值:
    3的二进制是 11
    5的二进制是 101
    加起来是 1000
    

    我们现在要理解一句话,如果看到一个二进制的数字。需要了解它是有符号数还是无符号数

    拓展:
    接下来先给大家扩展一个寄存器:里面可以存值。通用的寄存器有8个,可以存储任意的值。我可以通过mov指令向某个寄存器存值,如下图:

    在这里插入图片描述
    现在我要写一个-1,我们来看看会有怎样的区别:

    在这里插入图片描述
    这里的FFFF FFFF是我们存的-1在寄存器中的值。一个F就是 1111,首先最高位的值是1,所以代表他是一个负数。我们正常存1的时候首位明显是0,而存-1就变成FFFF FFFF。因为-1在计算机中是补码的方式存储的,所以负数在我们计算机中使用补码方式存储的。所以学习通过直接操作查看是最有效的

    FFFF FFFF代表三十二个1,如果是无符号的话,代表它是正数。如果是有符号的话,代表它是一个负数,是有很大的本质区别的。

    我们搞懂原码反码补码之后,以后知道了计算机底层是怎么存储数字的:正数就正常的存,因为无论原码反码补码,正数都是相同的。而负数主要存的是补码!如果了解了这些,就是为了接下来的位运算打交道。

    7.位运算

    我们之前说过,计算机现在可以存储所有的数字(正数,浮点数,字符),不论正数还是负数都可以存储。如果可以把这些数字加以运算,我们就可以做到世界上的一切。无论多复杂的运算,底层都是加减乘除。我们只要把位运算的位如何操作运算记住、突破就可以了。

    接下来我们学习位运算

    首先有一个面试高频题:2*8最高效的计算方式?
    这道题不论怎样都非常慢,只有通过位运算才是最快的,比如左移、右移。而且要记住一句话:很多底层的调试器(例如C语言调试器),当我们手写调试器的时候就需要通过来判断CPU的状态。

    位运算就是我们常说的与或非 异或运算等…我们一个一个来看:

    与运算
    在JAVA语言中用 & 符号去表示,但是在汇编中用 and 代表与。下面图片方便我们的理解:

    在这里插入图片描述

    1011 0001
    1101 1000
    -------------------- 与运算的结果
    1001 0000
    

    或运算
    在JAVA语言中用(|)表示,在汇编语言中用or表示,同样根据或运算也有一张电路图可以帮助理解:

    在这里插入图片描述

    1011 0001
    1101 1000
    --------------- 或运算
    1111 1001
    

    异或运算
    在我们JAVA语言中用(^)表示,在汇编语言中xor表示。说白了记住一句话:不一样就是1。再来一张电路图理解:

    在这里插入图片描述

    1011 0001
    1101 1000
    --------------------异或运算
    0110 1001
    

    非运算(单目运算符)
    我们所谓的取反(非),在JAVA语言中是(!),在C语言中是(~),在汇编语言中是not。
    说白了一句话:0就是1,1就是0。

    1101 1000
    ----------------- 非运算 
    0010 0111
    

    通过这些可以完成我们的加减乘除。怎么通过位运算实现加减乘除呢?

    位运算
    它是一个移动位,分为左移,右移。(左移*2,右移/2)。

    左移(shl <<):

    0000 0001  所有的二进制位全部左移若干位,高位就丢弃,低位补0
    0000 0010
    

    右移(shr >>):

    0000 0001  所有二进制全部右移若干位,低位就丢弃,高位补01(根据符号位决定,负数补1,正数补00000 0000
    
    如果想要取值(C++)
    int a = 10;
    printf("%d\n",a>>2);
    

    总结: 汇编的本质就是操作二进制。

    通过二进制、位运算实现 加减乘除。

    8.位运算的加减乘除

    接下来我们讲,如何通过位运算实现加减乘除。我们的计算机只认识0和1,但是基本的数学是建立在加减乘除上。(加法搞定,一切都搞定了)

    举个例子:求4+5?

    计算机是如何操作的呢?
    0000 0100
    0000 0101
    -------------------------(加法,计算机是不会直接加)
    0000 1001
    
    
    那么计算机的实现原理是什么呢?
    怎么将两个数加起来?核心是:异或。
    
    第一步,异或(不一样为1):如果不考虑进位,异或就可以直接出结果
    0000 0100
    0000 0101
    -------------------------
    0000 0001
    
    
    第二步,计算机在这个异或的结果上在做与运算操作:
    与运算(判断进位),如果与运算结果为0,那么就没有进位。
    0000 0100
    0000 0101
    -------------------------
    0000 0100
    
    
    第三步,将与运算的结果左移一位。
    0000 0100 ——> 0000 1000 (进位的结果)
    
    
    第四步,还是一个异或运算。(第一步异或出来的结果和第三步与运算的进位结果再一次异或)
    0000 0001
    0000 1000
    ------------------------
    0000 1001
    
    
    第五步,再去做一个与运算,判断它是否有进位。(与第二步一样)
    0000 0001
    0000 1000
    ------------------------
    0000 0000
    
    
    最后一步与运算结果为
    0000 0000
    电路都断了,灯泡全灭,通过不了,所以最终结果就是:
    与运算为0的结果的上一个异或运算结果:
    0000 1001 (二进制的9)
    
    
    如果与运算不为0,继续循环。
    

    举个例子:求4-5?

    我们说了,计算机没有所谓的减法,那么计算机是如何操作的呢?
    4-5 说白了就是 4+(-5)
    
    0000 0100
    1111 1011 (代表二进制的-5)
    ------------------------(减法,计算机是不会直接减)
    1111 111181就是ff,所以ff在十进制中代表-10000 0100
    1111 1011
    ------------------------ 异或(如果不考虑进位,异或就可以直接出结果)
    1111 1111
    
    
    0000 0100
    1111 1011
    ------------------------ 与运算(判断进位),如果与运算结果为0,那么就没有进位。
    0000 0000
    
    
    最终结果
    1111 1111 (十六进制的ff,十进制的-1)
    

    举个例子:
    乘法: x * y,本质就是y个x相加,还是加法。
    除法: x / y,本质就是减法,就是x能减去多少个y。

    结论:
    计算机只会做加法!

    9.汇编语言环境说明

    目前为止,我们可以从零设计一套自己的进制规则。自己设计电路来实现加减乘除。但是最终乘除的结果是一个二进制,例如:我们有32个灯泡,就可以显示32个数值,最终亮起灯泡的数就是最终的结果。手动转换这个结果和值!(十进制和二进制的转换)

    机器语言并不会做很多事情,它很“笨”。机器语言说白了就是位运算,(加减乘除)
    都是电路来实现的。这就是计算机最底层的本质。

    但是,我们发现,学完了这些东西依旧是不懂,只是对现在的程序做了一些提高的理解。但是想通过理解写程序写不出来,难道我们真的写不出来吗?

    通过机器语言来实现加法计算器,这就是设计电路

    我们通过指令来代替我们的一些二进制编码!比如说:刚才的加法运算是通过各种操作能否通过一个符号计算呢?比如说我就想叫它(ADD指令),假设我给计算机发一个ADD指令,它通过识别我的指令转换成机器语言(也就是编译)ADD指令转换为二进制操作。
    在这里插入图片描述
    汇编语言说白了,底层还是二进制,但是二进制写起来太麻烦了。这个时候我们通过汇编指令给计算机发一些操作,然后让计算机执行。这个地方就要涉及到编译器!因为我们说的编译命令不是机器直接能识别的,需要把命令转码,转成计算机认识的信息,不然没法识别。这个时候就涉及到编译器的发展

    如果学底层,一定不要用特别智能的编译器(IDEA,VSCODE等),就是用越远古的越好(记事本,vim等)。很多人学习C语言使用,用的是vim编辑器去写C语言,用gcc来执行。这是学习C的正确方式。底层的大佬几乎都是最原始的idea。

    在学习汇编之前,先要掌握环境的配置

    1. Vc6(程序到汇编的理解,通过C语言实现)
    2. OD
    3. 抓包工具
    4. 加密解密工具

    尽量不要使用java去学汇编,学完了汇编去学jvm就会觉得很简单。但是如果我学java再学汇编就有点痛苦,建议使用C++学汇编。因为C++可以直接看到内存的地址,可以打印通过指针直接找到内存的地址,通过地址判断信息。

    学汇编不是为了写代码,就是为了理解程序的本质
    如果懂汇编,就能够理解所有复杂的概念。

    在这里插入图片描述
    如果我们是一个做应用的程序员,别人调试不了的程序,如果学过汇编,都可以调试。因为知道底层堆栈到底做了什么。如果是做安全的(反外挂,反病毒),就要理解汇编到二进制的全部知识。

    现在的计算机至少是32位,还有的是64位。我们要知道,它是由32位演化过来的。底层架构没有发生变化,只是多了寄存器,主要是寻址能力增加

    汇编入门: 了解汇编和程序的对应关系,程序的本质即可。

    在这里插入图片描述
    学会了这些,不理解的java原码就理解了。汇编非常重要!这对我们向上学习有很大的帮助。有些编程技术学进不去,很大原因就是因为底层不够。底层精通了在学习编程语言就发现太轻松了!

    10.寄存器的理解

    学习汇编,要学习三个重要的概念:

    1. 寄存器
    2. 内存
    3. 汇编指令

    通用寄存器:可以存储任何值

    存储数据:CPU>内存>硬盘
    CPU分为32位和64位。

    • 32位:8 16 32
    • 64位:8 16 32 64

    32位的通用寄存器只有8个:
    在这里插入图片描述
    寄存器中的存值范围:0 ~ FFFFFFFF

    计算机如何向寄存器中存值呢?
    对于二进制来说,就是直接修改值。但是修改值需要找到对应的位置,所以才有内存地址

    mov指令

    mov  存的地址,存的数
    mov  存的地址1,存的地址1
    

    在这里插入图片描述
    可以将数字写入到寄存器,可以将寄存器中的值,写到寄存器。

    计算机的本质:计算力! 就是为了计算!(鼠标光标在移动都是在计算)

    不同的寄存器:

    32位代表八个F(FFFF FFFF),一个F代表4位(1111)
    
    
    		FFFF	FF    
    32168位
    EAX     AX		AL
    ECX		CX		CL
    EDX		DX		DL
    EBX		BX		BL
    ESP		SP		AH
    EBP		BP		CH
    ESI		SI		DH
    EDI		DI		BH
    

    对于8位:L代表低8位,H代表高8位
    16位是FFFF 高八位占前两个FF,低八位占后两个FF

    在这里插入图片描述
    除了这些通用寄存器之外,那么其他的寄存器每一位都有自己特定的功能(比如:开机关机)。
    我们一般写值都会写到通用寄存器中。

    11.内存

    寄存器很小,而且不够用。所以我们会把数据放到内存中。

    有句话:每个应用程序进程都有4GB的内存空间。 程序用的内存就是空头支票,虽然每个应用程序的进程都有4GB内存空间,但是真正到机器上使用时候并没有那么大。

    在这里插入图片描述
    程序真正运行的时候,才会用到物理内存。

    1B = 8bit
    1KB = 1024B
    1MB = 1024KB
    1GB = 1024MB

    假设是4GB内存电脑,就等于4096m => 最终计算为位,就是可以存储的最大容量。
    计算机中的内存地址很多,空间很大。

    内存地址:
    存一个数:占用大小,数据宽度。存到哪里呢?

    计算机中的内存地址很多,空间很大。我们要给空间取名字,每个空间分配一个地址,名字。

    在这里插入图片描述
    这些给内存起的编号就是我们的内存地址。32位(8个十六进制的值)

    32位:决定寻址能力!
    FFFFFFFF + 1 = 100000000,最大的值。
    位是怎么限制内存大小呢?
    100000000 内存地址 * 8位 :800000000。
    转换为十进制 / 8:4,294,967,296字节。
    按照规则/1024最终发现就是 4GB(不能超过)。
    64位,绰绰有余。

    所以每个内存地址都有一个编号:可以通过编号向里面存值

    在这里插入图片描述
    很多人学C语言搞不懂指针,原因就是 不懂内存

    内存如何存值?(mov指令)
    存值需要知道数据宽度(byte word dword),地址位置(自定义:0xFFFFFFFF)
    不是任意的地址都可以写东西,只有程序申请过的内存地址我们才可以使用。

    汇编如何向内存中写值:
    mov  数据宽度  内存地址,1
    
    mov  byte ptr ds:[0x19ff70],1
    
    传递的值的大小一定要和数据宽度要相等,如果大放不进去。
    

    在这里插入图片描述
    内存地址有多种写法

    1. ds:[0x19FF70+4](内存地址偏移):加 偏移(4),地址就变成了:0x19FF74
    2. ds:[eax](寄存器):把寄存器中的值放到内存中。
    3. ds:[eax + 4](寄存器偏移

    数组为例:
    ds:[reg + reg * {1,2,4,8}]
    ds:[reg + reg * {1,2,4,8} + 4] 偏移

    12.总结

    学到这里,在学其他的汇编内容已经很轻松了,包括学计算机操作原理也很轻松。
    如果能够全部理解,再看自己写的程序就会豁然开朗很多。

    展开全文
  • 计算机底层二进制

    千次阅读 2020-03-16 11:07:37
    计算机底层硬件不支持"负数" 利用补码编码规则实现"负数",“负数的计算” 补码是如何编码的: 以4位数编码为例讨论补码编码规则 4位补码没有实用价值 补码规则可以推广到32位int数 4位2进制计算时候,要保持4位数不变...

    2进制


    什么是2进制逢2进1的计数规则.

    10进制 与 2进制:
    在这里插入图片描述

    案例:

    public static void main(String[] args) {
        /*
         * 2进制
         */
        //编译时候 将10进制"50"转换为2进制
        //软件运行期间, i在内存中是2进制
        int i = 50;
        /*
         * toBinaryString 将整数i在内存中
         * 存储的2进制实际位数转换为字符串
         * 如需要展示一个整数在内存中实际
         * 的2进制情况时候就调用这个方法
         */
        System.out.println(
                Integer.toBinaryString(i));
        /* 
         * i在内存中就是2进制的 110010 
         * 而println方法在输出时候自动
         * 调用了 Integer.toString() 
         * 将2进制转换为10进制字符串,
         * 最终输出了十进制字符串 "50" 
         */
        System.out.println(i);//50
    
        for(i=0; i<=150; i++) { 
            System.out.println(
                Integer.toBinaryString(i));
        }
    }
    

    因为高位0被省略了,所以实际输出结果应该为:

    00000000 00000000 00000000 00000000
    00000000 00000000 00000000 00000001
    00000000 00000000 00000000 00000010
    00000000 00000000 00000000 00000011
    00000000 00000000 00000000 00000100
    00000000 00000000 00000000 00000101
    00000000 00000000 00000000 00000110
    00000000 00000000 00000000 00000111
    00000000 00000000 00000000 00001000
    00000000 00000000 00000000 00001001
    00000000 00000000 00000000 00001010
    00000000 00000000 00000000 00001011
    00000000 00000000 00000000 00001100
    00000000 00000000 00000000 00001101
    00000000 00000000 00000000 00001110
    00000000 00000000 00000000 00001111
    

    Java编程语言解决了人类与计算机沟通问题:
    在这里插入图片描述

    16进制


    16进制用于简写(缩写)2进制!

    1. 2进制直接书写冗长,繁琐,易错很不方便
    2. 16进制的基数是2进制基数的4次幂
    3. 简写规则: 2进制数字从低位到高位,每4个2进制数可以缩写为一个16进制数.

    原理:
    在这里插入图片描述

    案例:

    public static void main(String[] args) {
        /*
         * 16进制用于简写2进制
         */
        // Java 7 提供了 0b前缀用于声明
        // 2进制直接量
        int n = 0b110010; //50
        int m = 0b101101011110001001111101010101;
        //m可以缩写为i
        int i = 0x2d789f55;
    
        System.out.println(m==i);
        System.out.println(
                Integer.toBinaryString(m));
        System.out.println(
                Integer.toBinaryString(i));
    
    }
    

    补码


    为了解决"负数"问题,将固定位数的2进制数,分一半作为"负数"使用的编码规则,称为补码.

    补码的核心目的是解决 “负数” 的编码问题.

    1. 计算机底层硬件不支持"负数"
    2. 利用补码编码规则实现"负数",“负数的计算”

    补码是如何编码的:

    1. 以4位数编码为例讨论补码编码规则
    2. 4位补码没有实用价值
    3. 补码规则可以推广到32位int数
    4. 4位2进制计算时候,要保持4位数不变,多出的位数自动溢出
    5. 高位为0的作为正数, 值就是原始的2进制值
    6. 高位为1的作为负数, 从0倒推编码

    都是1的数是-1
    一个1以后都是0的数是最小值

    1. 因为高位可以判断正负数,所以称为是"符号位"
    2. 补码正负数互补对称(巧合): -n=~n+1

    原理:
    在这里插入图片描述

    案例:

    public static void main(String[] args) {
        /*
         * 补码的编码规律
         */
        int max = Integer.MAX_VALUE;
        int min = Integer.MIN_VALUE;
        System.out.println(
                Integer.toBinaryString(max));
        System.out.println(
                Integer.toBinaryString(min));
        System.out.println(
                Integer.toBinaryString(max+1));
        System.out.println(min);
        System.out.println(max);
    
    
        long lmax = Long.MAX_VALUE;
        long lmin = Long.MIN_VALUE;
        System.out.println(
                Long.toBinaryString(lmax));
        System.out.println(
                Long.toBinaryString(lmin));
    
    }
    

    经典面试题目:

    System.out.println(~-8);
    如上代码输出结果是( B ): A.6 B.7 C.8 D.9

    2进制运算


    运算符:

    ~ & | >>> >> <<

    与运算 & (and)
    基本规则(逻辑乘法), 有0则0

    0 & 0 = 0
    0 & 1 = 0
    1 & 0 = 0
    1 & 1 = 1
    

    计算时候需要将两个数对其位置, 对应的位进行与计算

    举个栗子:

    n =    01110111 11010010 10111000 10111010
    m =    00000000 00000000 00000000 11111111
    k=n&m  00000000 00000000 00000000 10111010
    

    如上案例的意义: k中的数据是n数据的最后8位数, 相当于将n的最后8位数切下存储到了k中. 如上计算称为"掩码计算",其中用于拆分数据的工具m称为 “掩码Mask”, 8个1称为8位掩码.

    案例:

    int n = 0x77d2b8ba;
    int m = 0xff;
    int k = n & m;
    System.out.println(
        Integer.toBinaryString(n));
    System.out.println(
        Integer.toBinaryString(m));
    System.out.println(
        Integer.toBinaryString(k));
    

    右移位计算 >>>
    运算规则: 将2进制数整体向右移动, 低位多出数字溢出舍弃, 高位补0.

    举个栗子:

    n =     01110111 11010010 10111000 10111010
    m=n>>>1 001110111 11010010 10111000 1011101 
    k=n>>>2 0001110111 11010010 10111000 101110
    g=n>>>8 00000000 01110111 11010010 10111000
    b3=(n>>>8) & 0xff;
    

    案例:

    int n = 0x77d2b8ba;
    int m = n>>>1;
    int k = n>>>2;
    int g = n>>>8;
    int b3 = (n>>>8) & 0xff;
    //按照2进制输出 n m k g b3
    

    将一个整数拆分为4个byte
    案例:

    public static void main(String[] args) {
        /*
         * 将整数拆分为 4个byte
         * raf.writeInt()方法的底层就是
         * 利用2进制计算将int拆分为4个byte
         * 然后写到文件中.
         */
        int n = 0x77d2b8ba;
        int b1 = (n>>>24) & 0xff;
        int b2 = (n>>>16) & 0xff;
        int b3 = (n>>>8) & 0xff;
        int b4 = n & 0xff;
        System.out.println(
                Integer.toBinaryString(n));
        System.out.println(
                Integer.toBinaryString(b1));
        System.out.println(
                Integer.toBinaryString(b2));
        System.out.println(
                Integer.toBinaryString(b3));
        System.out.println(
                Integer.toBinaryString(b4));
    }
    

    | 或计算
    基本规则(逻辑加法), 有1则1

    0 | 0 = 0
    0 | 1 = 1
    1 | 0 = 1
    1 | 1 = 1
    

    将两个2进制数对齐位数,对应位进行或计算.

    举例子:

    n =   00000000 00000000 00000000 10110111
    m =   00000000 00000000 10111110 00000000
    k=n|m 00000000 00000000 10111110 10110111 
    

    如上代码的意义: 将两个8位数拼接在一起

    b1 =   00000000 00000000 00000000 11001110
    b2 =   00000000 00000000 00000000 10111010
    b3 =   00000000 00000000 00000000 11110110
    b4 =   00000000 00000000 00000000 10001101
    
    b1<<24 11001110 00000000 00000000 00000000 
    b2<<16 00000000 10111010 00000000 00000000
    b3<<8  00000000 00000000 11110110 00000000 
    b4     00000000 00000000 00000000 10001101
    
    n = (b1<<24)|(b2<<16)|(b3<<8)|b4;
    

    案例:

    int n = 0xb7;
    int m = 0xbe00;
    int k = n|m;
    int b1 = 0xce;
    int b2 = 0xba;
    int b3 = 0xf6;
    int b4 = 0x8d;
    int i = (b1<<24)|(b2<<16)|(b3<<8)|b4;
    //验证: 输出n m k b1 b2 b3 b4 i 的2进制
    

    >>> 和 >> 区别移位的数学意义:

    1. 2进制数字向左移动一次, 数值扩大两倍
    2. 2进制数字向右移动一次, 数值缩小两倍

    案例:

    int n = 50;
    int m = n<<1;//100
    int k = n<<2;//200
    int g = n<<3;//400
    

    在这里插入图片描述
    栗子:

    n =     00000000 00000000 00000000 00110010 50
    m=n>>1  000000000 00000000 00000000 0011001 25
    k=n>>2  0000000000 00000000 00000000 001100 12
    a=n>>>1 000000000 00000000 00000000 0011001 25
    b=n>>>2 0000000000 00000000 00000000 001100 12
    
    n =     11111111 11111111 11111111 11001110 -50
    m=n>>1  111111111 11111111 11111111 1100111 -25 
    k=n>>2  1111111111 11111111 11111111 110011 -13
    a=n>>>1 011111111 11111111 11111111 1100111 2147483647-24  
    

    ArrayList 扩容规则: 每次扩充1.5倍 = n + n>>1 = n + n/2

    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    

    经典面试题目:

    可以将 n * 8 优化为( n<<3 )
    可以将 n / 2 替换为( n>>1 ), n是大于0的数
    

    i=i++


    栗子:

    int i = 5;
    i = i++;
    System.out.println(i); //5
    
    i = 5;
    System.out.println(i++); //5
    System.out.println(i);//6
    

    i=i++ 的计算步骤分析:

    在这里插入图片描述

    展开全文
  • 作为程序员这些年写过很多代码,但在一个阳光明媚的午后我盯着自己屏幕上的代码脑海里浮现出了一个疑问,“这些代码在底层到底是怎么运行起来的,我写的每一行代码到底是什么意思?” 然而我并没有答案,尽管大部分...

    大家好,我是小风哥。

    作为程序员这些年写过很多代码,但在一个阳光明媚的午后我盯着自己屏幕上的代码脑海里浮现出了一个疑问,“这些代码在底层到底是怎么运行起来的,我写的每一行代码到底是什么意思?

    然而我并没有答案,尽管大部分情况下我的代码“看起来”好像也能“正确”完成工作,可是一旦遇到一些相对“高级”的问题时往往束手无策,比如程序运行Core Dump、内存泄漏、程序运行起来很慢等等,这个思考结果着实让我大吃一惊吓出一身冷汗,我竟然对自己所写的代码“一无所知”

    于是我的脑海里紧接着就出现了一个画面,自己就是那个手持火把穿过炸药厂幸存下来而不自知的傻瓜。

    仔细思考后我找到了问题所在,自己的知识体系一直存在漏洞,或者干脆就没有形成知识体系,于是我决定好好研究一下计算机底层知识,并在学习过程中将其分享出来,于是就形成了知乎上的这些文章。

    1. 你管这破玩意叫 CPU ?

    2. 看完这篇还不懂高并发中的线程与线程池你来打我(内含20张图)

    3. 终于明白了,一文彻底理解I/O多路复用

    4. 从小白到高手,你需要理解同步与异步(内含10张图)

    5. 10张图让你彻底理解回调函数

    6. 函数运行时在内存中是什么样子?

    7. 程序员应如何理解高并发中的协程

    8. 线程间到底共享了哪些进程资源?

    9. 线程安全代码到底是怎么编写的?

    10. 自己动手实现一个malloc内存分配器 | 30图

    11. 特斯拉遇上 CPU:程序员的心思你别猜

    12. CPU 空闲时在干嘛?

    13. 10 个内存引发的大坑,你能躲开几个?

    14. 18张图揭秘高性能Linux服务器内存池技术是如何实现的

    15. 神秘!申请内存时底层发生了什么?

    16. CPU 是如何理解 01 二进制的?

    17. 数据结构是如何装入 CPU 寄存器的?

    18. mmap可以让程序员解锁哪些骚操作?

    19. CPU 核数与线程数有什么关系?

    20. CPU可以跑多快?地球到火星的距离告诉你!

    ...

    剩下的不再列举了有很多CSDN的朋友问有没有pdf版本,我也整理出来了,绘图非常精美,我为其专门设计了封面,并将其命名为《计算机底层的秘密》,现在免费分享给大家。

     

    可以使用这个下载链接点击下载《计算机底层的秘密》

    PS:整理该系列不易,如果我的文章对你有一点帮助或者启发,希望可以给我点个赞!

    展开全文
  • 1 数据概述 百度给我们的定义是数据就是数值,也就是我们通过观察、实验或计算...诺依曼计算机体系的,所以数据在计算机底层存储都是按照二进制,也就是0和1存储的,而我们日常生活中使用的都是十进制,因此数据是需要...
  • 计算机底层是什么东西?

    万次阅读 多人点赞 2018-09-13 15:19:16
    代码写着写着,突然特别想知道计算机底层到底是什么东西。 最近特意看了一些文章,谨以此文章记录一下自己的想法。 ①数据传输 首先先大概描述一下 数据传输过程 A计算机(服务器)<-------->网络传输...
  • 计算机底层知识.docx

    2021-10-10 16:25:27
    计算机底层知识.docx
  • 并发资料
  • 计算机实现的方法 1、求异或 0000 0100 0000 0101 ------------------ 0000 0001 2、再对这两个数求与运算(运算结果不为0,则判断发生了进位,为0则没有进位) 0000 0100 0000 0101 ------------------...
  • 计算机底层加法/乘法实现

    千次阅读 2020-06-10 17:08:24
    计算机底层加法/乘法实现存储方式原理加法乘法除法代码实现加法乘法 存储方式 计算机底层中存放数字使用二进制形式,负数使用补码(反码+1)来存放. 原理 加法 两个二进制的相加结果是用一个异或门实现的; 两个二...
  • 计算机底层&操作系统核心知识总结

    千次阅读 2022-04-25 19:39:25
    计算机底层&操作系统核心知识总结 总结的知识是适合开发者必备的一些基础知识。 计算机组成原理 一、早期的计算机 1.1 第一代计算机 最早的计算机可以追溯到图灵的计算机理论上去,也就是说图灵的计算机理论是...
  • 对一个程序员来说,计算机组成原理、数据结构、算法、操作系统等几个课程,是必备的基础知识,位列在各大学计算机系培养计划的核心课程里。其中,“计算机组成原理”更是入门和底层知...
  • 计算机底层架构(偏硬件)综述

    千次阅读 2019-01-08 15:04:26
    目前整理的计算机底层相关的笔记知识,是紧紧围绕现代计算机模型而来的,主要会涉及cpu,多级存储和I/O外设。由于cpu指令系统(汇编语言)和cpu的联系过于紧密,也会有一定涉及。本文主要对计算机模型进行综述。 ...
  • 计算机底层的存储方式:所有数字在计算机底层都以二进制形式存在。 二进制数据的存储方式:所有的数值,不管正负,底层都以补码的方式存储。 计算机原码:直接将一个数值转换为二进制,最高位是符号位。符号位正数...
  • 《编码:隐匿在计算机硬件背后的语言》 《深入理解计算机系统》 语言:C java 《C程序设计语言》《C primer Plus》 数据结构与算法:《java数据结构与算法》 《算法》 《算法导论》 《计算机程序设计艺术》 操作...
  • 有很多民族自豪感爆棚的兄弟会把算盘当成计算机的起源,还有爆破天的兄弟会把阴阳当成二进制0和1的起源,我觉得这件事儿就有点儿不靠谱了 如果非要追究计算机的鼻祖,那就得讲讲17世纪前欧洲的故事,最早的计算机...
  • 学习计算机底层的意义

    千次阅读 2022-04-27 17:07:54
    大学期间学习底层的意义
  • java编程入门之计算机底层运行过程 我想对于大多数初入门的或已经学习编程一段时间的程序员来说,他们对于计算机底层一定充满好奇,或者称之为迷茫吧!为什么我写的程序计算机能够认识?为什么计算机能够帮助我们...
  • 计算机底层知识拾遗(一)理解虚拟内存机制 http://blog.csdn.net/iter_zc/article/details/42644229 这个系列会总结计算机,网络相关的一些重要的底层原理。很多底层原理大家上学的时候都学过,但是在...
  • 计算机底层结构

    2021-11-28 17:54:57
    1.机器语言(计算机底层) 最初的计算机所使用的是由“0”和“1”组成的二进制数,二进制是计算机的语言的基础。计算机发明之初,计算机只能被少部分人使用,人们需要用0、1组成的指令序列交由计算机执行,对于机器...
  • 这是一本详尽地讲解汇编语言的书,很实用,希望对您能有所帮助。This is a classic book about assembly language, It's so practical that everybody should read it many times,I hope it would be helpful for ...
  • 这个系列的目的也就是从整体上来理解计算机底层硬件和操作系统的一些重要的组件是如何工作的,从而来指导应用层的开发。这篇讲讲文件系统的重要概念,为后面的IO系统做铺垫。 文件系统主要有三类 1. 位于磁盘...
  • 计算机被称为20世纪最伟大的发明之一 。1946年诞生的第一台电子计算机ENIAC,是一个每秒能运行5000次、重达30吨的庞然大物。如今计算机变得无处不在,以至于人们大大低估了它的复杂性一今天一 部几百克的普通手机包含...
  • 内核同步机制 关于同步理论的一些基本概念 临界区(critical area): 访问或操作共享数据的代码段 简单理解:synchronized大括号中部分(原子性) 竞争条件(race conditions)两个线程同时拥有临界区的执行权 ...
  • 计算机操作系统底层相关知识

    千次阅读 多人点赞 2021-11-22 20:24:00
    输入、输出设备:输入设备向计算机输入数据,计算机经过计算之后,把数据输出给输出设备。期间,如果输入设备是键盘,按下按键时是需要和CPU进行交互的,这时就需要用到控制总线了。 2.线路位宽与CPU位宽 数据是...
  • 计算机网络底层原理分析详解

    千次阅读 2020-09-05 11:29:56
    DNS协议底层依赖UDP协议,udp首部较小,4个字段,8个字节:包含源端口(本机随机生成的一个端口) 和 目的端口(53号端口)。 传输层依赖于下层的网络层,网络层将数据进一步打包,打包成一个IP报文,会加上ip首部:...
  • 计算机底层书籍三件套--大话计算机

    千次阅读 2021-02-04 23:37:21
  • 文章开头我只想问一个问题:写了很多代码,你不好奇代码在计算机底层是什么样的吗? 下面将一步一步探索这个过程。首先,大家要有个初步的认识,一次从左到右进行编译、汇编。 高级语言(C语言) 汇编语言 二进制...
  • 计算机底层实现的一些理解思路

    千次阅读 2015-11-18 13:40:45
    很久都在纠结,学计算机知识例如一个编程语言)只知道这么做是对的,但不能说出来为什么? 基础不好,从中间学起,前后搭不上,知其然不知其所以然! 先说点最底层的实现的一些理解思路。 从底层用“叠加...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 297,537
精华内容 119,014
关键字:

计算机底层

友情链接: TSP-TS.zip