底层视角看C语言
文章目录
- main函数很普通
- main函数之前调用了什么
- main函数和自定义函数的对比
- 变量名只为人而存在
- goto是循环的本质
- 指针变量
- 指针是一个特殊的数字
- 汇编层面看指针
- 数组和指针
- 数组越界问题
- 低端地址越界
- 高端地址越界
- 引用就是指针
main函数很普通
main函数是第一个被调用的函数吗?在用户视角看来main函数的确程序的入口,但是在CPU视角下,main函数仅仅只是一个普通函数,和用户自定义的其他函数没有任何的区别。
main函数之前调用了什么
Linux环境:
_start->__libc_start_main->main

每一个Linux进程的入口函数都是_start,_start是一段直接由汇编语言编写的函数,它负责的工作就是把程序的命令行参数以及环境变量压入栈中,此时环境变量和参数一起存放在一个数组中
为了把环境变量单独提取出来,_start紧接着会调用__lib_start_main函数构建一张环境变量表,并进行一些全局变量的初始化工作,随后再进入main函数执行用户程序,再main函数退出时进行收尾操作例如全局变量的释放。这么一来,main函数似乎也只不过是一个被调用的函数,它只是默认被注册为用户代码的入口而已,也就是说,用户代码入口不一定非要是main函数
main函数和自定义函数的对比
一直以来我们编写C/C++程序时都是约定俗成地添加一个main函数来启动程序(因为不这么做往往会报错),这种情况一度让不少人认为main函数具有特殊的地位,能够得到CPU的青睐,其实不然,CPU眼中main函数啥也不是就是很普通的函数
int main(){return 0;
}
int func(){return 0;
}
通过汇编观察main和func的区别,会发现它们所对应的汇编指令居然完全一致
main:push rbpmov rbp, rspmov eax, 0pop rbpret
func:push rbpmov rbp, rspmov eax, 0pop rbpret
2个函数所做的操作都是一样的
- 建立函数栈帧 push rbp / mov rbp,rsp
- 将返回值拷入寄存器 mov eax,0
- 释放函数栈帧并返回 pop rbp / ret
gcc有一个命令可以改变用户代码的入口,使得用户指定其他函数作为程序起点
gcc -nostartfiles -efunc test.c
意思是编译test.c不使用系统的标准启动文件,将程序起点设置为func函数;一般不推荐这么做,因为使用标准启动文件代表着你需要自己为func瞻前顾后,这无疑是在自找麻烦
变量名只为人而存在
变量对程序员来说并不陌生,我们无时无刻都在使用变量帮助我们记忆,因为一个好的变量名可以大大提高源代码的可读性;尽管如此,对于可执行文件来说它并不需要存储所谓的变量名(release模式编译链接),CPU只需要知道一个逻辑地址就可进行读写操作,也就是说在发布模式编译链接时所有的变量名都会被转换称逻辑地址。
因此,我们可以给出关于变量的定义:=变量就是逻辑地址的一个别名,它向上以字符串形式以供人阅读记忆,向下被转成地址值供CPU访存
int a=0;
int main(){a=2;return 0;
}
所对应的汇编文件
main:push rbpmov rbp,rspmov DWORD PTR [rip+0x0],0x2 # e <main+0xe>//将立即数0x2写入到rip值偏移量为0的位置,DWORD PTR标识4字节mov eax,0x0pop rbpret
可以看出a=2这条代码所对应的汇编是mov DWORD PTR [rip+0x0],0x2,CPU只需要通过几个逻辑地址相对寻址就可以确定内存的哪个位置需要被赋值为2
goto是循环的本质
虽然说不鼓励在编写C/C++程序时随意的使用goto,但是不代表goto不值得探究,早期的循环其实都是通过goto语句来实现的,只不过随着程序越来越大,过多的goto语句打破了程序的结构性使得源码难以维护,进而衍生出了结构性更强的for、while、do语句,它们都是在底层实现上都继承的goto的机制
void test_for(){for(int i=0;i<10;++i){}
}
void test_while(){int i=0;while(i<10) ++i;
}
void test_do(){int i=0;do{}while(++i<10);
}
void test_goto(){int i=0;goto L1;
L2:if(i<10) goto L1;return ;
L1:++i;goto L2;
}
对应的汇编代码
test_for:
//...mov DWORD PTR [rbp-4], 0jmp .L2
.L3:add DWORD PTR [rbp-4], 1
.L2:cmp DWORD PTR [rbp-4], 9jle .L3
//...
test_while:
//...mov DWORD PTR [rbp-4], 0jmp .L5
.L6:add DWORD PTR [rbp-4], 1
.L5:cmp DWORD PTR [rbp-4], 9jle .L6
//...
test_do:
//...mov DWORD PTR [rbp-4], 0
.L8:add DWORD PTR [rbp-4], 1cmp DWORD PTR [rbp-4], 9jle .L8
//...
test_goto:
//...mov DWORD PTR [rbp-4], 0jmp .L10
.L14:nop
.L10:add DWORD PTR [rbp-4], 1nopcmp DWORD PTR [rbp-4], 9jle .L14
//...
除了标签值不一样外,可以说基本上是一模一样
jmp指令是无条件跳转,对应进入循环体
cmp指令作作比较,add指令对应+1
jle指令是有条件跳转,负责继续or结束循环
指针变量
指针可以说是C语言的精髓所在,正是因为指针,使得C语言称为最灵活的高级语言,它使得用户可以自由的对一个内存区域进行读写(读写是否合法是另一码事),为了更好的理解指针变量,我们把指针变量这个名词拆解为指针+变量,变量上面提过它是地址的别名,指针其实就是一个地址值,因此所谓的定义一个指针变量的本质就是在一块内存空间上写入一个地址值(这和写入一个普通数没有什么区别)
指针是一个特殊的数字
地址值的本质就是数字,只不过它可以被用于访存(解引用),CPU可以先通过读取存放指针的那一块内存得到其中的地址值,再解引用该地址读写内存

汇编层面看指针
int a=1;
int main(){int* p=&a;*p=2;int** pp=&p;*pp=0;return 0;
}
main:
//...mov QWORD PTR [rbp-16], OFFSET FLAT:a //把a的地址写入地址[rbp-16]处,QWORD PTR标识8字节mov rax, QWORD PTR [rbp-16] //读取rbp-16地址处的值放入寄存器rax(a的地址)mov DWORD PTR [rax], 2 //解引用lea rax, [rbp-16]mov QWORD PTR [rbp-8], rax mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], 0
//...
无论是几级指针的解引用,本质上都没有什么不同,都是从一块内存中获得另一块内存的位置进行访存
数组和指针
C语言中所有的数组传参最终都会退化成指针传参,因此传入多大的数组,最终在一个函数内部所看到的都是一个大小固定的指针
void fun1(int arr[5]){arr[3]=1;}
void fun2(char arr[100]){arr[3]=1;}
void fun3(double arr[1024]){arr[3]=1;}
汇编代码
fun1:
//...mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]add rax, 12 //3*4mov DWORD PTR [rax], 1
//...
fun2:
//...mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]add rax, 3 //3*1mov BYTE PTR [rax], 1
//...
fun3:
//...mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]add rax, 24 //3*8movsd xmm0, QWORD PTR .LC0[rip]movsd QWORD PTR [rax], xmm0
//...
数组索引操作的本质就是解引用,因此例子中的三个函数等价于
void fun1(int* arr){arr[3]=1;}
void fun2(char* arr){arr[3]=1;}
void fun3(double* arr){arr[3]=1;}
所谓的索引操作只不过是一个偏移量,用于指针的加减操作(指针变量加1减1的跨度取决于指向的类型,如果是int就移动4字节,char就移动1字节,double则是8字节)
数组越界问题
指针作为C语言的精髓,同时也是C语言最危险的一面,原则上一旦获取到地址就可以进行访存,但不能保证目标地址的数据是否可以被安全覆盖,如果一旦不小心将一些重要的内存空间刷新就有可能导致进程崩溃甚至更严重的后果。这种行为称之为野指针非法寻址,所谓野指针就是一个不应该被读写内存空间的地址,野指针最容易出现的场景就是数组越界。
虽然数组越界问题很危险,不过好在随着编译器进步,大部分数组越界问题都能在编译阶段得到拦截。
低端地址越界
void func1(){int a[2];a[1]=1;a[0]=2;a[-1]=3;a[-2]=4;
}
void func2(){int b[4];b[3]=1;b[2]=2;b[1]=3;b[0]=4;
}
int main(){func1();printf("have a good day\n");return 0;
}
很明显func1中存在数组越界访问的问题,但是它可能可以运行不会有段错误(可以看到have a good day,高版本编译器在编译阶段就直接报错),之所以正常运行的原因是因为虽然func1对一个非法区域进行了写入操作,但是碰巧这一块区域没有任何有效数据,所以不会出错
汇编代码
func1:
//...mov DWORD PTR [rbp-4], 1mov DWORD PTR [rbp-8], 2mov DWORD PTR [rbp-12], 3mov DWORD PTR [rbp-16], 4
//...
func2:
//...mov DWORD PTR [rbp-4], 1mov DWORD PTR [rbp-8], 2mov DWORD PTR [rbp-12], 3mov DWORD PTR [rbp-16], 4
//...
如果编译可以通过,通过查看汇编代码可以知道-1、-2索引操作偷偷地拓展了数组的长度使之变成4,并且是向低地址拓展的(rbp保存栈底指针,栈向低地址增长),这种越界称为低端地址越界,它可能不会造成程序崩溃,但大概率会影响到结果正确性(因为func1修改了本不属于它的栈空间,这可能造成其他局部变量数据失效)
高端地址越界
为了更好地就是高端地址越界,需要先稍微了解一下函数栈帧概念,每一个函数有一个栈空间称为栈帧,函数中所需要的局部变量都存储在栈帧中,当被调函数返回时需要销毁栈帧,CPU的指令寄存器恢复至主调函数。因此在建立新的函数栈帧时必须保存当前的指令地址以供后续返回,这个返回地址通过紧邻着被调函数栈帧的栈底。如果被调函数意外的修改了这里的值,就会发生意外(意外地执行恶意代码或段错误);这种越界称为高端地址越界
void evil(){//恶意代码printf("evil\n");exit(1);//让进程意外结束
}
void func1(){int a[2];a[2]=(int)evil; //将返回值设置为一个恶意函数//a[2]=0,将返回值设置为0地址处,这里没有合法指令就报段错误a[1]=1;a[0]=2;
}
int main(){func1();printf("have a good day\n");return 0;
}
(如果编译可以通过)main函数调用func1后进程就结束了,没有输出have a good day,即func1回不到main了,转而走到evil

引用就是指针
C++中引入的引用是对指针操作的简化,但其实只是在语法层面上做了一层封装和少量限制(没有多级引用和空引用,引用二次引用其他对象)
void f1(int* x){*x=1;}
void f2(int& x){x=1;}
void f3(int&& x){x=1;}
汇编代码
f1(int*):
//...建立栈帧mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]mov DWORD PTR [rax], 1
//...释放栈帧
f2(int&):
//...mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]mov DWORD PTR [rax], 1
//...
f3(int&&):
//...mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]mov DWORD PTR [rax], 1
//..
通过汇编可以很明显的看到f1和f2的赋值操作完全一致,引用也是通过获得地址后解引用才能实现对外部变量的修改,左值引用和右值引用在汇编实现上没有什么区别,只不过对于右值引用的修改是针对于一个被延长声明周期的临时对象做修改。
一句话描述引用:引用变量就是一个指针,对于引用变量的修改就是解引用操作
————————————————————————————
相关文章:
底层视角看C语言
文章目录 main函数很普通main函数之前调用了什么main函数和自定义函数的对比 变量名只为人而存在goto是循环的本质指针变量指针是一个特殊的数字汇编层面看指针 数组和指针数组越界问题低端地址越界高端地址越界 引用就是指针 main函数很普通 main函数是第一个被调用的函数吗&…...
【点云学习笔记】——分割任务学习
3D点云实例分割 vs 3D点云语义分割 1. 功能对比 代码1(实例分割):用于3D点云中的实例分割任务,其目标是将点云中的物体分割成独立的实例。每个实例可能属于相同类别但需要被分开,比如在自动驾驶中的多个行人、汽车&am…...
Qt——窗口
一.窗口概述 Qt 窗口是通过 QMainWindow 类来实现的。 QMainWindow是一个为用户提供主窗口程序的类,继承QWidget类,并且提供一个预定义的布局。包含一个菜单栏(menu bar),多个工具栏(tool bars࿰…...
InfluxDB性能优化指南
1. 引言 1.1 InfluxDB的简介与发展背景 InfluxDB是一个开源的时间序列数据库(TSDB),由InfluxData公司开发,专门用于处理高频率的数据写入和查询。其设计初衷是为物联网、应用程序监控、DevOps和实时分析等场景提供一个高效的存储…...
负载均衡式在线oj项目开发文档2(个人项目)
judge模块的框架 完成了网页渲染的功能之后,就需要判断用户提交的代码是否是正确的,当用户点击提交之后,就会交给路由模块的/judge模块,然后这个路由模块就需要去调用jude模块了,也就是需要一个新的jude模块ÿ…...
ssm081高校实验室管理系统的设计与实现+vue(论文+源码)_kaic
毕 业 设 计(论 文) 题目:高校实验室管理系统的设计与实现 摘 要 互联网发展至今,无论是其理论还是技术都已经成熟,而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播,搭配信息管理工具可以很…...
GitLab基于Drone搭建持续集成(CI/CD)
本文介绍了如何为 Gitee 安装 Drone 服务器。服务器打包为在 DockerHub 上分发的最小 Docker 映像。 1. 准备工作 创建OAuth应用 创建 GitLab OAuth 应用。Consumer Key 和 Consumer Secret 用于授权访问极狐GitLab 资源。 ps:授权回调 URL 必须与以下格式和路径匹配&…...
用GPB外链打造长期稳定的SEO优势!
很多人在谈外链时,总喜欢纠结是追求数量还是追求质量。其实,最理想的策略是两者兼顾。而在这其中,GPB外链可以说是长期SEO提升的“法宝”。为什么这么说?因为GPB外链不仅保证了高质量,还附带了与网站主题高度相关的原创…...
第11章 内连接与外连接
一、介绍内连接与外连接 (1)内连接与外连接介绍 1、内连接:合并具有同一列的两个以上的表的行, 结果集中不包含一个表与另一个表不匹配的行。 2、外连接:: 两个表在连接过程中除了返回满足连接条件的行以外还返回左(…...
C++ 游戏开发:打造高效、性能优越的游戏世界
在游戏开发领域,C 一直是最受欢迎的编程语言之一。其高效的内存管理和对硬件的底层控制,使得 C 成为开发高性能游戏的首选语言。从大型 3D 游戏引擎到独立游戏的制作,C 在游戏开发中发挥了不可替代的作用。 本文将带你了解 C 在游戏开发中的…...
太速科技-440-基于XCVU440的多核处理器多输入芯片验证板卡
基于XCVU440的多核处理器多输入芯片验证板卡 一、板卡概述 本板卡系我司自主研发的基于6U CPCI处理板,适用于多核处理器多输入芯片验证的应用。芯片采用工业级设计。 基于XCVU440T的多核处理器多输入芯片验证板卡基于6U CPCI架构,是单机中的一个…...
澳鹏通过高质量数据支持 Onfido 优化AI反欺诈功能
“Appen 在 Onfido 的发展中发挥了至关重要的作用,并已成为我们运营的重要组成部分。我们很高兴在 Appen 找到了可靠的合作伙伴。” – Onfido 数据和分析总监 Francois Jehl 简介:利用人工智能和机器学习增强欺诈检测 在当今日益数字化的世界ÿ…...
基于ECS实例搭建Hadoop环境
环境搭建: 【ECS生长万物之开源】基于ECS实例搭建Hadoop环境-阿里云开发者社区 搭建Hadoop环境_云服务器 ECS(ECS)-阿里云帮助中心 Hadoop入门基础(二):Hadoop集群安装与部署详解(超详细教程)࿰…...
关于vue如何监听route和state以及各自对应的实际场景
一、监听route 场景:监听浏览器地址栏分页参数的变化 // 注意 newPageNum和 oldPageNum是 string类型 $route.query.pageNum(newPageNum, oldPageNum) {if (newPageNum ! oldPageNum && newPageNum ! this.pageNum.toString()) {this.handleCurrentChange(p…...
【计网不挂科】计算机网络期末考试(综合)——【选择题&填空题&判断题&简述题】完整题库
前言 大家好吖,欢迎来到 YY 滴计算机网络 系列 ,热烈欢迎! 本章主要内容面向接触过C的老铁 本博客主要内容,收纳了一部门基本的计算机网络题目,供yy应对期中考试复习。大家可以参考 欢迎订阅 YY滴其他专栏!…...
Linux(CentOS)设置防火墙开放8080端口,运行jar包,接收请求
1、查看防火墙状态 systemctl status firewalld 防火墙开启状态 2、运行 jar 包,使用8080端口 程序正常启动 3、使用 postman 发送请求,失败 4、检查端口是否开放(需更换到 root 用户) firewall-cmd --zonepublic --query-por…...
对比:生成对抗网络(GANs)和变分自编码器(VAEs)
以下是生成对抗网络(GANs)和变分自编码器(VAEs)的详细介绍、区别、优缺点的对比表: 项目生成对抗网络(GANs)变分自编码器(VAEs)定义GANs 是一种生成模型,通过…...
sqlserver inner join on 条件是包含 怎么写
LEFT JOIN T_Customer tc on CHARINDEX(tbd.CluePhoneNumber,tc.u_phone)>0...
开源 AI 智能名片 S2B2C 商城小程序在微商内容展示中的应用与价值
摘要:本文围绕微商在社群和朋友圈这一“店面”的内容展示展开深入讨论,剖析展示对产品的热爱、产品真实反馈和代理反馈的重要意义,并详细阐述开源 AI 智能名片 S2B2C 商城小程序如何助力微商优化这些内容展示,从而提升微商营销效果…...
Codeforces Round 984 (Div. 3) (A~E)
文章目录 A. Quintomania思路code B. Startup思路code C. Anya and 1100思路code D. I Love 1543思路code E. Reverse the Rivers思路code https://codeforces.com/contest/2036 A. Quintomania 思路 签到题,直接模拟即可 code void solve(){int n;cin >>…...
rknn优化教程(二)
文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK,开始写第二篇的内容了。这篇博客主要能写一下: 如何给一些三方库按照xmake方式进行封装,供调用如何按…...
Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...
《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)
CSI-2 协议详细解析 (一) 1. CSI-2层定义(CSI-2 Layer Definitions) 分层结构 :CSI-2协议分为6层: 物理层(PHY Layer) : 定义电气特性、时钟机制和传输介质(导线&#…...
Android第十三次面试总结(四大 组件基础)
Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成,用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机: onCreate() 调用时机:Activity 首次创建时调用。…...
《C++ 模板》
目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...
JVM 内存结构 详解
内存结构 运行时数据区: Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器: 线程私有,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 每个线程都有一个程序计数…...
安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖
在Vuzix M400 AR智能眼镜的助力下,卢森堡罗伯特舒曼医院(the Robert Schuman Hospitals, HRS)凭借在无菌制剂生产流程中引入增强现实技术(AR)创新项目,荣获了2024年6月7日由卢森堡医院药剂师协会࿰…...
LLMs 系列实操科普(1)
写在前面: 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容,原视频时长 ~130 分钟,以实操演示主流的一些 LLMs 的使用,由于涉及到实操,实际上并不适合以文字整理,但还是决定尽量整理一份笔…...
Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...
解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...
