当前位置: 首页 > news >正文

底层视角看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个函数所做的操作都是一样的

  1. 建立函数栈帧 push rbp / mov rbp,rsp
  2. 将返回值拷入寄存器 mov eax,0
  3. 释放函数栈帧并返回 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&#xff08;实例分割&#xff09;&#xff1a;用于3D点云中的实例分割任务&#xff0c;其目标是将点云中的物体分割成独立的实例。每个实例可能属于相同类别但需要被分开&#xff0c;比如在自动驾驶中的多个行人、汽车&am…...

Qt——窗口

一.窗口概述 Qt 窗口是通过 QMainWindow 类来实现的。 QMainWindow是一个为用户提供主窗口程序的类&#xff0c;继承QWidget类&#xff0c;并且提供一个预定义的布局。包含一个菜单栏&#xff08;menu bar&#xff09;&#xff0c;多个工具栏&#xff08;tool bars&#xff0…...

InfluxDB性能优化指南

1. 引言 1.1 InfluxDB的简介与发展背景 InfluxDB是一个开源的时间序列数据库&#xff08;TSDB&#xff09;&#xff0c;由InfluxData公司开发&#xff0c;专门用于处理高频率的数据写入和查询。其设计初衷是为物联网、应用程序监控、DevOps和实时分析等场景提供一个高效的存储…...

负载均衡式在线oj项目开发文档2(个人项目)

judge模块的框架 完成了网页渲染的功能之后&#xff0c;就需要判断用户提交的代码是否是正确的&#xff0c;当用户点击提交之后&#xff0c;就会交给路由模块的/judge模块&#xff0c;然后这个路由模块就需要去调用jude模块了&#xff0c;也就是需要一个新的jude模块&#xff…...

ssm081高校实验室管理系统的设计与实现+vue(论文+源码)_kaic

毕 业 设 计&#xff08;论 文&#xff09; 题目&#xff1a;高校实验室管理系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很…...

GitLab基于Drone搭建持续集成(CI/CD)

本文介绍了如何为 Gitee 安装 Drone 服务器。服务器打包为在 DockerHub 上分发的最小 Docker 映像。 1. 准备工作 创建OAuth应用 创建 GitLab OAuth 应用。Consumer Key 和 Consumer Secret 用于授权访问极狐GitLab 资源。 ps:授权回调 URL 必须与以下格式和路径匹配&…...

用GPB外链打造长期稳定的SEO优势!

很多人在谈外链时&#xff0c;总喜欢纠结是追求数量还是追求质量。其实&#xff0c;最理想的策略是两者兼顾。而在这其中&#xff0c;GPB外链可以说是长期SEO提升的“法宝”。为什么这么说&#xff1f;因为GPB外链不仅保证了高质量&#xff0c;还附带了与网站主题高度相关的原创…...

第11章 内连接与外连接

一、介绍内连接与外连接 &#xff08;1&#xff09;内连接与外连接介绍 1、内连接&#xff1a;合并具有同一列的两个以上的表的行, 结果集中不包含一个表与另一个表不匹配的行。 2、外连接&#xff1a;: 两个表在连接过程中除了返回满足连接条件的行以外还返回左&#xff08…...

C++ 游戏开发:打造高效、性能优越的游戏世界

在游戏开发领域&#xff0c;C 一直是最受欢迎的编程语言之一。其高效的内存管理和对硬件的底层控制&#xff0c;使得 C 成为开发高性能游戏的首选语言。从大型 3D 游戏引擎到独立游戏的制作&#xff0c;C 在游戏开发中发挥了不可替代的作用。 本文将带你了解 C 在游戏开发中的…...

太速科技-440-基于XCVU440的多核处理器多输入芯片验证板卡

基于XCVU440的多核处理器多输入芯片验证板卡 一、板卡概述 本板卡系我司自主研发的基于6U CPCI处理板&#xff0c;适用于多核处理器多输入芯片验证的应用。芯片采用工业级设计。 基于XCVU440T的多核处理器多输入芯片验证板卡基于6U CPCI架构&#xff0c;是单机中的一个…...

澳鹏通过高质量数据支持 Onfido 优化AI反欺诈功能

“Appen 在 Onfido 的发展中发挥了至关重要的作用&#xff0c;并已成为我们运营的重要组成部分。我们很高兴在 Appen 找到了可靠的合作伙伴。” – Onfido 数据和分析总监 Francois Jehl 简介&#xff1a;利用人工智能和机器学习增强欺诈检测 在当今日益数字化的世界&#xff…...

基于ECS实例搭建Hadoop环境

环境搭建&#xff1a; 【ECS生长万物之开源】基于ECS实例搭建Hadoop环境-阿里云开发者社区 搭建Hadoop环境_云服务器 ECS(ECS)-阿里云帮助中心 Hadoop入门基础&#xff08;二&#xff09;&#xff1a;Hadoop集群安装与部署详解&#xff08;超详细教程&#xff09;&#xff0…...

关于vue如何监听route和state以及各自对应的实际场景

一、监听route 场景&#xff1a;监听浏览器地址栏分页参数的变化 // 注意 newPageNum和 oldPageNum是 string类型 $route.query.pageNum(newPageNum, oldPageNum) {if (newPageNum ! oldPageNum && newPageNum ! this.pageNum.toString()) {this.handleCurrentChange(p…...

【计网不挂科】计算机网络期末考试(综合)——【选择题&填空题&判断题&简述题】完整题库

前言 大家好吖&#xff0c;欢迎来到 YY 滴计算机网络 系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 本博客主要内容&#xff0c;收纳了一部门基本的计算机网络题目&#xff0c;供yy应对期中考试复习。大家可以参考 欢迎订阅 YY滴其他专栏&#xff01;…...

Linux(CentOS)设置防火墙开放8080端口,运行jar包,接收请求

1、查看防火墙状态 systemctl status firewalld 防火墙开启状态 2、运行 jar 包&#xff0c;使用8080端口 程序正常启动 3、使用 postman 发送请求&#xff0c;失败 4、检查端口是否开放&#xff08;需更换到 root 用户&#xff09; firewall-cmd --zonepublic --query-por…...

对比:生成对抗网络(GANs)和变分自编码器(VAEs)

以下是生成对抗网络&#xff08;GANs&#xff09;和变分自编码器&#xff08;VAEs&#xff09;的详细介绍、区别、优缺点的对比表&#xff1a; 项目生成对抗网络&#xff08;GANs&#xff09;变分自编码器&#xff08;VAEs&#xff09;定义GANs 是一种生成模型&#xff0c;通过…...

sqlserver inner join on 条件是包含 怎么写

LEFT JOIN T_Customer tc on CHARINDEX(tbd.CluePhoneNumber,tc.u_phone)>0...

开源 AI 智能名片 S2B2C 商城小程序在微商内容展示中的应用与价值

摘要&#xff1a;本文围绕微商在社群和朋友圈这一“店面”的内容展示展开深入讨论&#xff0c;剖析展示对产品的热爱、产品真实反馈和代理反馈的重要意义&#xff0c;并详细阐述开源 AI 智能名片 S2B2C 商城小程序如何助力微商优化这些内容展示&#xff0c;从而提升微商营销效果…...

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 思路 签到题&#xff0c;直接模拟即可 code void solve(){int n;cin >>…...

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...

golang循环变量捕获问题​​

在 Go 语言中&#xff0c;当在循环中启动协程&#xff08;goroutine&#xff09;时&#xff0c;如果在协程闭包中直接引用循环变量&#xff0c;可能会遇到一个常见的陷阱 - ​​循环变量捕获问题​​。让我详细解释一下&#xff1a; 问题背景 看这个代码片段&#xff1a; fo…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

UDP(Echoserver)

网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法&#xff1a;netstat [选项] 功能&#xff1a;查看网络状态 常用选项&#xff1a; n 拒绝显示别名&#…...

Android15默认授权浮窗权限

我们经常有那种需求&#xff0c;客户需要定制的apk集成在ROM中&#xff0c;并且默认授予其【显示在其他应用的上层】权限&#xff0c;也就是我们常说的浮窗权限&#xff0c;那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...

[Java恶补day16] 238.除自身以外数组的乘积

给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O(n) 时间复杂度…...

在WSL2的Ubuntu镜像中安装Docker

Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包&#xff1a; for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...

sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!

简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求&#xff0c;并检查收到的响应。它以以下模式之一…...

LeetCode - 199. 二叉树的右视图

题目 199. 二叉树的右视图 - 力扣&#xff08;LeetCode&#xff09; 思路 右视图是指从树的右侧看&#xff0c;对于每一层&#xff0c;只能看到该层最右边的节点。实现思路是&#xff1a; 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...