C语言:高级语言怎样抽象执行逻辑
平时我们做编程的时候,底层 CPU 如何执行指令已经被封装好了,因此你很少会想到把底层和语言编译联系在一起。但从我自己学习各种编程语言的经历看,从这样一个全新视角重新剖析 C 语言,有助于加深你对它的理解。
本节首先要了解 CPU 执行指令的过程,然后再来分析 C 语言的编译过程,掌握 C 语言的重要组成,最后我们再重点学习 C 语言如何对程序以及程序中的指令和数据进行抽象,变成更易于人类理解的语言(代码从这里下载)。
CPU执行指令的过程
CPU执行一条特定指令的详细过程是取指、译码、执行、访存、回写,这是一个非常详细的硬件底层细节,本节将从软件逻辑的角度看CPU执行多条指令的过程。
这个过程描述起来很简单,就是一个循环:
1.以PC寄存器中值为内存地址A,读取内存地址A中的数据
2.CPU把内存地址A中的数据作为指令执行,具体执行过程:取指、译码、执行、访存、回写
3.将PC寄存器中的值更新为内存地址A+(一条指令占用的字节数)
4.回到第一步
上述过程就是CPU执行指令的逻辑过程。
动手来写几行代码,调试一下,观察一下内存的内容和 CPU 寄存器的变化,代码如下:
.text
.global main
main: # main函数
add t1,zero,1 # x6 = 1
add t2,zero,2 # x7 = 2
add t0,t1,t2 # x5 = x6 + x7
add a0,zero,zero # x10 = 0 相当于main函数中的return 0
ret
这是一段 RV32I 指令集编写的汇编代码,设置好断点,执行如下:
看到 t0、t1、t2 寄存器中的数据和我们预期的一样。PC 寄存器从 0x10120,一直变化到 0x1012c,每执行一条指令 PC 寄存器的值都要加 4,这是因为每条 RV32I 指令都占用 4 字节的内存空间。
在调试控制台中执行-exec x/16xb 0x10120
命令,即可显示从 0x10120 开始的 16 字节内存数据,刚好 4 条指令的数据
下图展示了内存中的情况:
大致逻辑是这样的:最开始,由 CPU 控制单元通过控制总线发出要读取数据的控制信号;接着通过地址总线发送地址信号(当前情况下地址数据来源 PC 寄存器(0x10120));然后通过数据总线传送指令数据 (0x00100313);最后执行单元拿到指令数据开始执行,并增加 PC 寄存器使之指向下一条指令。重复这个过程,内存中的指令就能一条一条地执行了。
C语言编译过程
因为 CPU 始终只认识那些二进制数据,就需要把高级语言转化成为二进制数据,这个转化的过程叫编译过程,完成这个转化的工具软件就叫编译器。
比如C语言编译器编译C语言的过程,先通过下图来理解下这个过程,建立一个整体印象:
现代的 C 预处理器、C 编译器、汇编器、链接器是独立的程序,可以分开独立工作,并不是一个程序完成上图中所有的工作。
因为我们不开发编译器,这里你不需要理解词法、语法是如何分析的,中间代码是怎样优化的。我们要关注的焦点是,从 C 源代码到二进制机器指令数据的转化过程。
C语言的重要组成
想要弄清楚 C 如何跟二进制指令数据转化,首先要清楚 C 语言的重要组成部分
从不同层次抽象,里面的内容是不一样的:从高层次看代码中只有声明和定义,下一层看代码只有函数和变量,变量进一步分解还有不同的类型。
硬背这些分类只会让你晕头转向。接下来我们不妨分析一下,想要让一段 C 语言代码编译通过,需要哪些重要成分和逻辑结构。
在 C 语言中经常容易混淆声明和定义这两个概念,先来看看声明
- 声明是给变量、函数、结构体等命名,表明在程序代码中有该变量、函数、结构体,来看看下图中的代码:
在 declaration.c 程序文件中,声明了一个整型变量,一个结构体变量,一个函数。然后我们编译它,确实能编译成功,这说明在 C 语言文法中仅仅需要有声明就可以,当然空文件也是可以的。声明不会分配内存空间。
需要注意的是,只有声明的代码确实能编译成功,但链接的时候就不一定了,我们这里之所以能链接成功。是因为在其它代码中没有对这些声明进行了引用。
- 定义是具体给变量分配内存空间。这个内存空间可以是初始化的,也可以是没有初始化的、给出具体函数的实现。
具体函数可以是空函数,函数中没有语句什么都不做也可以,唯一必需的就是指明结构体成员。结构体也是变量,只不过结构体是多个变量的组合,同样要分配内存空间,可以初始化也可以不做初始化。
写代码验证一下对不对,如下图所示:
还是在 definition.c 程序文件中,定义了一个整型变量,一个结构体变量,一个函数。我们同样能成功编译它。这说明 C 语言文法中没有声明,只有定义也可以成功编译的,其实 C 语言文法的原则是,声明可以出现很多次,定义有且只能出现一次。声明和定义也可以同时出现。
编译的其中一个过程,就是用某种编程语言的文法来检查所写语言(代码)是否正确。你可以这么理解,语言的文法就是对这种语言的最高抽象,所以我们可以说 C 语言最重要的组成部分就是声明或者定义。
声明或者定义中又包含变量和函数,变量又有指针、数组、结构体,它们又包含各种类型,而函数中包含了各种表达式,各种表达式对变量进行操作。
编译器的语法分析过程,就是这样层层递归推导下去,最终构建出语法树,从而检查语言是否正确无误、是否符合该语言文法的规则定义,都符合编译才能通过。就像你学英文一样,你怎么判断一条英语句子是否正确呢?你会拿主谓宾等等约定俗成的语法去套,如果能套上去,就是正确的。
C语言对程序的抽象
看到 C 语言中,包含声明和定义,可以声明变量和函数,由图中绿色箭头指向。也可以定义变量和函数,由图中蓝色箭头指向,注意定义只能出现一次,声明可以出现多次。
故意安排指针在最前端,是因为从 C 语言特性讲,指针能指向任一变量和函数,由图中红色箭头指向;从另一个角度看,指针就是内存,能自由寻址读写内存空间,但能否读写内存则要看操作系统给的权限,指针就是 C 语言中的“上帝之手”。同时,图中黑色线条还表示指针可以有相应的类型,并且能参与运算,这是我把指针放在比函数更高位置的原因。
需要注意的是,各种类型的变量是可以定义在函数以外的,这些定义在函数以外的变量是全局变量,而定义在函数内部的变量叫局部变量。
下面继续研究一下表达式。从前面的图里,可以看到 C 语言表达式包含了变量和运算符。
变量又有各种类型,单个变量也是表达式,但是运算符不能单独存在变成表达式,所以 C 语言表达式要么是单个变量,要么是变量加运算符一起。根据运算符的类型不同,可以分成运算表达式、逻辑表达式、赋值表达式等。
举例:
int sumdata = 0;//全局整型变量sumdata
void func()
{
int i = 1;//局部整型变量i
int *p; //局部整型指针变量p
p = &sumdata;//把sumdata变量的地址赋值给p变量,从而指向sumdata变量
while(1)//循环流程控制
{
if(i > 100)
{
break;//跳出循环,流程控制
}
(*p) += i;//相当于sumdata = sumdata + i
i = i + 1;
}
return;
}
上述代码所有的表达式中,涉及了一个全局变量,两个局部变量。其中局部变量中有一个是指针变量,指向全局的变量。包含了更多的流程控制语句,可以明显地看到表达式就是:变量和运算符组合在一起,完成了对变量的操作。而变量代表了数据,最终就能实现对数据的运算。但是变量有各种类型,这些类型只是规范了变量的位宽和大小。
小结
C 语言是如何抽象程序的,如下表所示。
C 语言就是函数 + 变量。函数表示算法操作,变量存放数据,即数据结构,合起来就是程序 = 算法 + 数据结构。
C语言:高级语言怎样抽象执行逻辑的更多相关文章
- scala(二) Future执行逻辑解读
在scala中是没有原生线程的,其底层使用的是java的Thread机制.但是在scala中对java Thread进行了封装,实现了更便于操作线程的Future. 官方文档: Futures pro ...
- defer、return、返回值,这三者的执行逻辑
defer.return.返回值,这三者的执行逻辑是: return 最先执行,return 负责将结果写入返回值中:接着defer执行,可能修改返回值:最后函数携带当前返回值退出.
- ETL-kettle 核心执行逻辑
一.大数据下的ETL工具是否还使用Kettle kettle 作为通用的ETL工具,非常成熟,应用也很广泛,这里主要讲一下 目前我们如何使用kettle的? 在进行大数据处理时,ETL也是大数据处理的 ...
- Springboot中mybatis执行逻辑源码分析
Springboot中mybatis执行逻辑源码分析 在上一篇springboot整合mybatis源码分析已经讲了我们的Mapper接口,userMapper是通过MapperProxy实现的一个动 ...
- [C语言]进阶|数据类型: 整数, 浮点, 逻辑, 类型转换和条件运算
--------------------------------------------------------------------------------- [C语言的类型] 1. 整型(都分为 ...
- Fabric架构:抽象的逻辑架构与实际的运行时架构
Fabric从1.X开始,在扩展性及安全性上面有了大大的提升,且新增了诸多的新特性: 多通道:支持多通道,提高隔离安全性. 可拔插的组件:支持共识组件.权限管理组件等可拔插功能. 账本数据可被存储为多 ...
- 并发包的线程池第一篇--ThreadPoolExecutor执行逻辑
学习这个很长时间了一直没有去做个总结,现在大致总结一下并发包的线程池. 首先,任何代码都是解决问题的,线程池解决什么问题? 如果我们不用线程池,每次需要跑一个线程的时候自己new一个,会导致几个问题: ...
- MVC+Ef项目(4) 抽象业务逻辑层BLL层
接下来,我们就要到业务逻辑层了,简单的说,业务逻辑层就是调用Repository(可以看做是DAL数据库访问层) 先来看看项目的架构 我们现在就开始来做BLL层. 同样,先编写 UserInfoS ...
- Python语言基础04-构造程序逻辑
本文收录在Python从入门到精通系列文章系列 学完前面的几个章节后,博主觉得有必要在这里带大家做一些练习来巩固之前所学的知识,虽然迄今为止我们学习的内容只是Python的冰山一角,但是这些内容已经足 ...
- 使用CyclicBarrier+线程池,按总页数分批次开多线程执行逻辑
通过CyclicBarrier+线程池的方式,同步的方式分页分批次并发高效处理逻辑,将总页数分成多个批次并发执行每页逻辑,每个批次处理DO_MAX_SIZE个页,每个批次等待DO_MAX_SIZE个页 ...
随机推荐
- Hadoop - HDFS 概述
什么是HDFS HDFS的优缺点 HDFS的文件块大小 HDFS的写数据流程 HDFS的副本配置策略 HDFS读数据的流程 什么是HDFS HDFS(Hadoop Distributed File S ...
- 读论文-新闻推荐系统:近期进展、挑战与机遇的评述(News recommender system_ a review of recent progress, challenges, and opportunities)
前言 今天读的论文为一篇于2022年发表在"人工智能评论"(Artificial Intelligence Review)的论文,文章主要强调了NRS面临的主要挑战,并从现有技术中 ...
- AI回答:一个简洁的php中间件类
<?php class MiddlewareStack { private $middlewares = []; private $request; private $response; /** ...
- idea社区版配置springboot项目问题分析及处理
前言 记录一次使用IDEA社区版配置SpringBoot项目的经历,包括遇到的问题及解决过程 IDEA版本:IntelliJ IDEA 2024.2.3 (Community Edition) 问题描 ...
- osharp多租户方案
osharp多租户方案 租户信息 using System; using System.Collections.Generic; using System.Linq; using System.Tex ...
- 🧠ChatGPT 中文提示语大全【超全 Prompt 用法】- 已分类
基于 awesome-chatgpt-prompts 进行分类,更方便查找自己所需,炼制适合自己的ChatGPT法宝. 工作 简历写手 我需要你写一份<2年工作经验的前端工程师>的简历,你 ...
- protobuf优缺点及编码原理
什么是protobuf protobuf(Google Protocol Buffers),官方文档对 protobuf 的定义:protocol buffers 是一种语言无关.平台无关.可扩展的序 ...
- docker swarm CA证书到期
1.现象 在portain平台查看日志,发现一些节点日志无法查看报错为:Error grabbing logs: rpc error: code = Unknown desc = warning: i ...
- VMware虚拟化的CPU调度原理及实践建议
简介: ESXi的CPU调度原理及实践建议 ESXi的CPU调度原理 CPU调度器的设计目标 公平性:确保虚机按照各自配置的份额占用物理CPU.吞吐量:最大化物理CPU的使用率.响应性:vCPU从'就 ...
- BUUCTF---世上无难事
1.题目 给出一大串密文,说flag藏在其种并且是32位 2.解题 结合题目所给信息,flag在其中32位,并且语句通顺,想来是移位密码,需要加偏移量Mod的那种,最后数字应该代表了key is XX ...