RISC-V处理器设计(五)—— 在 RISC-V 处理器上运行 C 程序

news2024/11/22 21:54:17

目录

一、前言

二、从 C 程序到机器指令

三、实验

3.1 实验环境

3.11 Windows 平台下环境搭建

3.12 Ubuntu 平台下环境搭建

3.13 实验涉及到的代码或目录

3.2 各文件作用介绍

3.2.1 link.lds

3.2.2 start.S

3.2.3 lib 和 include 目录

3.2.4 common.mk

3.2.5 demo 目录

3.3 上板测试

第一种:直接作为 FPGA 比特流的一部分下载到板子上。

第二种:使用串口烧录程序。

四、写在最后


一、前言

前面我们使用 verilog 完成了一个 risc-v cpu 的设计,但 cpu 最终也是为了程序服务的,不能执行程序的 cpu 没有任何意义。所以这一节我们要研究如何在自己设计的 cpu 上运行 C 程序。项目仓库如下:

risc-v-cpu: 一个基于 RISC-V 指令集的 CPU 实现(成功移植到野火征途 PRO 开发板),以及从零开始写一个基于 RISC-V 的 RT-Thread~ - Gitee.com

本节涉及到的代码都在仓库的 rt-thread 目录下。

二、从 C 程序到机器指令

当然,cpu 肯定不能直接执行 C 程序,cpu 只能识别机器语言,机器语言就是由 0 和 1 组成的一条二进制序列,比如 ADD 指令有如下格式:

在确定 rs1、rs2、rd 三个寄存器后,就能确定 ADD 指令的二进制序列,cpu 能够识别并执行这个二进制序列。 risc-v 指令集相关内容可以看我的这篇文章:
RISC-V处理器的设计与实现(一)—— 基本指令集_risc_v-CSDN博客

那么我们如何从 C 程序得到我们想要的机器指令呢?答案就是使用编译器,编译分为如下四个阶段(程序从编译到运行-CSDN博客):

即预处理阶段、生成汇编代码阶段、汇编阶段、链接阶段,你可能会问为什么图里面的编译器只参与了一个阶段的工作,这是因为现在的编译工具功能非常强大,比如 linux 下的 gcc,我们只需要一行指令就能帮我们完成四个阶段的操作:

gcc hello.c

最终生成的 a.out 即为操作系统(如 Linux、Ubuntu)上的可执行程序,此时的 a.out 虽然能在 像 Linux 这种操作系统上运行,但是 cpu 并不能直接识别并运行,编译生成的 a.out 大多是 ELF 格式。

ELF 格式的可执行文件包含了很多 cpu 不能识别的信息,但像 Linux 这样的操作系统可以识别这些信息,比如判断该可执行文件可以在哪种架构上运行,该可执行文件的各个段的位置在哪等等。关于 ELF 文件的解析可以看如下博客:

ELF文件详解—初步认识-CSDN博客

为了得到 cpu 能直接识别的内容,我们可以通过 objcopy 指令将生成的 a.out 转变为去掉了这些无用信息的 hello.bin 文件,具体怎么操作可以看我的这篇博客:

开发一个RISC-V上的操作系统(一)—— 环境搭建_riscv操作系统开发_Patarw_Li的博客-CSDN博客

同时,如果要编译出我们 risc-v 架构的 cpu 能识别的机器指令,我们还需要选择对应的交叉编译器,如 Ubuntu 20.04 下可以使用官方提供的 riscv64-unknown-elf-gcc。如果你使用的是 x86 下的gcc,那么编译出来的机器指令则只能由 x86 架构的 cpu 识别。

三、实验

实验的目录在本仓库的 rt-thread 目录下,该目录下新增了一个 demo 目录,可供用户自己设计 C 程序到本 cpu 上运行。其他的 experiment 目录都是和 rt-thread 移植相关的,后续也会更新相应的文章。

3.1 实验环境

下面是两种平台下的编译环境搭建,大家可以根据自己的情况自行选择。 

3.11 Windows 平台下环境搭建

  1. GNU 工具链(链接:https://pan.baidu.com/s/1Bdmn-FH0T7ekm2kMxkzJTw?pwd=qn69 提取码:qn69),百度云下载解压后,将 bin 目录添加到环境变量里即可。
  2. make 工具(链接:https://pan.baidu.com/s/1X-F1BVPMa3-B-V1EHB4tEQ?pwd=418d 提取码:418d),百度云下载解压后,将 bin 目录添加到环境变量里即可。
  3. Python 3.7

3.12 Ubuntu 平台下环境搭建

Ubuntu 版本:

$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.2 LTS
Release:	20.04
Codename:	focal
 
$ uname -r
5.15.0-76-generic

安装Ubuntu 20.04官方提供的 GNU工具链:

sudo apt update
sudo apt install build-essential gcc make perl dkms git gcc-riscv64-unknown-elf

并且要将 Makefile 里面的 include ../common.mk 修改为 include ../common_ubuntu.mk

3.13 实验涉及到的代码或目录

  1. include:公共头文件目录;
  2. lib:公共函数目录;
  3. start.S:启动文件;
  4. link.lds:链接脚本;
  5. common.mk:Makefile 的公共部分(Windows 平台下);
  6. common_ubuntu.mk:Makefile 的公共部分(Ubuntu 平台下);
  7. demo:用户可以在本目录下编写能在本 CPU 上运行的 C 程序;

3.2 各文件作用介绍

3.2.1 link.lds

link.lds 被称为链接脚本,是编译器链接步骤的重要部分。 官方文档

链接操作都是由链接脚本(Linker Script)所控制的,按照官方的话来说,链接脚本用来描述 input file(比如 hello.c 和 printf.c,编译器会将他们分别编译成 hello.o 和 printf.o,这两个文件就是链接操作的输入)中的每个 section 应该如何被映射到 output file(最终生成的可执行文件)中,并且控制 output file 中的内存布局。

我们可以自己编写链接脚本,也可以使用默认的链接脚本,如果要使用自己编写的链接脚本,则需要在编译时使用 -T 参数来指定。

关于链接脚本的语法大家可以自行查阅资料,这里我只介绍对于本次实验比较重要的部分:

1. ENTRY 来确定程序的入口为 _start,该符号在 start.S 中声明:

ENTRY(_start)

2. MEMORY 部分,这部分是根据 soc 上的 flash 和 ram 在总线上的起始地址以及所支持的空间大小来配置的。比如本 cpu 的 rom 起始地址为 0x00000000,大小为 16KB:

MEMORY
{
  flash (rxai!w) : ORIGIN = 0x00000000, LENGTH = 16K
  ram   (wxa!ri) : ORIGIN = 0x10000000, LENGTH = 8K
}

3. 定义程序栈大小,如果其他地方没有 __stack_size 的定义就把栈大小定义为 2KB:

 __stack_size = DEFINED(__stack_size) ? __stack_size : 2K;

4. 有些段是可读可写的,比如 .data 段,这些段是不能放到 flash 上的(flash 只读),所以我们在链接脚本中会把 .data 段指定到 ram 上存储(使用 >ram):

 .lalign         :
  {
    . = ALIGN(4);
    PROVIDE( _data_lma = . );
  } >flash AT>flash 

  .dalign         :
  {
    . = ALIGN(4);
    PROVIDE( _data = . );
  } >ram AT>flash 

  .data          :
  {
    *(.rdata)

    *(.gnu.linkonce.r.*)
    *(.data .data.*)
    *(.gnu.linkonce.d.*)
    . = ALIGN(8);
    PROVIDE( __global_pointer$ = . + 0x800);
    *(.sdata .sdata.*)
    *(.gnu.linkonce.s.*)
    . = ALIGN(8);
    *(.srodata.cst16)
    *(.srodata.cst8)
    *(.srodata.cst4)
    *(.srodata.cst2)
    *(.srodata .srodata.*)
  } >ram AT>flash 

  . = ALIGN(4);
  PROVIDE( _edata = . );
  PROVIDE( edata = . );

但这样会导致生成的二进制文件中间产生很大的空洞,因为 flash 和 ram 的地址一般是不同的,所以我们需要使用 AT>(关于 AT> 的作用可以看这篇博客https://www.cnblogs.com/LogicBai/p/16982841.html),这样可以把 .data 段先放到 flash 中,然后在启动文件 start.S 中通过 _data_lma、_data、_edata(分别对应 .data 段在 flash 中的实际地址、在 ram 上的逻辑起始地址、在 ram 上的逻辑末尾地址)这三个地址来把 .data 段从 flash 上搬运到 ram 上,这样就可以将数据存储的位置和运行的位置区分开来

5. 因为 .bss 段的数据都为 0,所以无需占用存储空间,只需要保存 .bss 段的运行时的逻辑起始地址和末地址(__bss_start 和 _end),然后在启动时使用  start.S 将起始地址到末地址中间的内容初始化为 0 即可:

  PROVIDE( __bss_start = . );
  .bss            :
  {
    *(.sbss*)
    *(.gnu.linkonce.sb.*)
    *(.bss .bss.*)
    *(.gnu.linkonce.b.*)
    *(COMMON)
    . = ALIGN(4);
  } >ram AT>ram 

  . = ALIGN(8);
  PROVIDE( _end = . );

3.2.2 start.S

启动文件 start.S 作为 cpu 上电复位后第一个执行的程序,主要完成以下工作:

  • 初始化 gp (global pointer) 全局指针寄存器、sp (stack pointer) 栈指针寄存器;
  • 将 .data 段的数据从 flash 中加载至 RAM 中;
  • 清空 bss 段数据;
  • 进入 main 函数运行;

start.S 的代码如下: 

.section .init;
.globl _start;
.type _start, @function

_start:
.option push
.option norelax
    la gp, __global_pointer$
.option pop
    la sp, _sp

    /* 把 data section 从 flash 搬运到 ram 中 */
    la a0, _data_lma
    la a1, _data
    la a2, _edata
    bgeu a1, a2, 2f
1:
    lw t0, (a0)      /* 从 flash 中取出一个 word 的数据 */
    sw t0, (a1)      /* 将取出的数据存入 ram 中对应位置 */
    addi a0, a0, 4
    addi a1, a1, 4
    bltu a1, a2, 1b
2:
    /* 将 bss section 初始化为 0 */
    la a0, __bss_start
    la a1, _end
    bgeu a0, a1, 2f
1:
    sw zero, (a0)
    addi a0, a0, 4
    bltu a0, a1, 1b
2:

    /* 调用初始化函数 */
    call _init
    /* 跳转到 main */
    call main

/* never came here */
loop:
    j loop

第 8 行, 加载全局指针寄存器 gp。

第 10 行,加载栈指针寄存器 sp。

第 13 ~ 22 行,把 .data 段从 flash 搬运到 ram 中。

第 25 ~ 31 行,将 .bss 段初始化为 0。

第 35 行,调用初始化函数,定义在 init.c 中。

第 37 行,调用 main 函数,执行用户编写的程序。

第 40、41 行,为了防止执行用户程序后,cpu 跑飞的情况,最后写了一个死循环。

3.2.3 lib 和 include 目录

这两个目录主要提供一些公共的函数,比如串口、printf 函数等,用户可以在 main.c 文件里面包含头文件后使用里面定义的函数。

3.2.4 common.mk

该文件为 Makefile 编译脚本的公共部分,所有 Makefile 文件(比如 demo 里面的 Makefile)都会包含 common.mk。

common.mk 的内容如下: 

CROSS_COMPILE = riscv-none-embed-

RISCV_GCC     := $(CROSS_COMPILE)gcc
RISCV_AS      := $(CROSS_COMPILE)as
RISCV_GXX     := $(CROSS_COMPILE)g++
RISCV_OBJDUMP := $(CROSS_COMPILE)objdump
RISCV_GDB     := $(CROSS_COMPILE)gdb
RISCV_AR      := $(CROSS_COMPILE)ar
RISCV_OBJCOPY := $(CROSS_COMPILE)objcopy
RISCV_READELF := $(CROSS_COMPILE)readelf

.PHONY: all
all: $(TARGET)

ASM_SRCS += $(COMMON_DIR)/start.S

C_SRCS += $(COMMON_DIR)/init.c 
C_SRCS += $(COMMON_DIR)/lib/uart.c 
C_SRCS += $(COMMON_DIR)/lib/printf.c
C_SRCS += $(COMMON_DIR)/lib/hw_timer.c


LINKER_SCRIPT := $(COMMON_DIR)/link.lds

INCLUDES += -I$(COMMON_DIR)

LDFLAGS += -T $(LINKER_SCRIPT) -nostartfiles -Wl,--gc-sections -Wl,--check-sections

ASM_OBJS := $(ASM_SRCS:.S=.o)
C_OBJS := $(C_SRCS:.c=.o)
LINK_OBJS += $(ASM_OBJS) $(C_OBJS)
LINK_DEPS += $(LINKER_SCRIPT)

CLEAN_OBJS += $(TARGET) $(LINK_OBJS) $(TARGET).dump $(TARGET).bin ../$(TARGET).inst

CFLAGS += -march=$(RISCV_ARCH)
CFLAGS += -mabi=$(RISCV_ABI)
CFLAGS += -mcmodel=$(RISCV_MCMODEL) -nostdlib -ffunction-sections -fdata-sections -fno-builtin-printf -fno-builtin-malloc -Wall

$(TARGET): $(LINK_OBJS) $(LINK_DEPS) Makefile
	$(RISCV_GCC) $(CFLAGS) $(INCLUDES) $(LINK_OBJS) -o $@ $(LDFLAGS)
	$(RISCV_OBJCOPY) -O binary $@ $@.bin
	$(RISCV_OBJDUMP) --disassemble-all $@ > $@.dump
	python ../../tools/bin_to_mem.py $@.bin ../$@.inst

$(ASM_OBJS): %.o: %.S
	$(RISCV_GCC) $(CFLAGS) $(INCLUDES) -c -o $@ $<

$(C_OBJS): %.o: %.c
	$(RISCV_GCC) $(CFLAGS) $(INCLUDES) -c -o $@ $<

.PHONY: clean
clean:
	rm -f $(CLEAN_OBJS)

第 1 ~ 10 行,配置工具链。

第 15 ~ 20 行,指定需要编译的公共汇编文件和 C 文件。

第 23 行,配置链接脚本的路径。

第 25 行,指定公共头文件目录。

第 27 行,指定链接参数

  1. -nostartfiles:指定链接时不要使用标准的系统启动文件,自定义入口函数(_start)时必须使用 -nostartfiles 选项进行链接。
  2. -Wl,--gc-sections:在链接生成最终可执行文件时,如果带有-Wl,--gc-sections参数,并且之前编译目标文件时带有-ffunction-sections-fdata-sections参数,则链接器ld不会链接未使用的函数,从而减小可执行文件大小。
  3. --check-sections:检查段地址是否重叠 (默认)。

3.2.5 demo 目录

此目录下包含了 main.c 文件和 Makefile,在此目录下执行 make 命令即可生成 demo.bin 和 demo.inst 文件,下面上板测试会用到这两个文件。

main.c 文件的内容是一个简单的加法,然后输出加法的结果,用户可以改成自己的 C 程序:

/* 头文件声明 */
#include "../include/printf.h"
#include "../include/uart.h"

/* main 函数 */
int main(void)
{
    int a = 1;
    int b = 2;
    int c = a + b;
    printf("The result of c: %d\n", c);

    /* stop here */
    while(1){};
}

3.3 上板测试

有两种将编译好的二进制程序在本 CPU 上执行的方法。

第一种:直接作为 FPGA 比特流的一部分下载到板子上。

将 FPGA/rtl/perips/rom.v 文件里面的如下部分的注释打开,并且将路径改为生成的指令文件 demo.inst 的路径:

重新编译后,直接烧录到板子上即可: 

使用串口工具连接板子,配置好串口号和波特率并打开串口后,按下板子上的复位键即可看到输出: 

第二种:使用串口烧录程序。

将 demo.bin 二进制文件复制到 serial_utils/binary 目录下,然后进入 serial_utils 目录,先按住 key1 不动: 

然后使用命令行执行如下命令烧录 demo.bin 文件,烧录完成即可松开 key1:

# 这里的 COM 号要根据你自己的来选,我这里是 COM3
python .\serial_send.py COM3 .\binary\demo.bin

使用串口工具连接板子,配置好串口号和波特率并打开串口后,按下板子上的复位键即可看到输出: 

两种方法相比,第一种方法更为稳妥,第二种方法更为灵活,这里更建议大家使用第一种方法,在程序出问题的时候,第一种方法还可以使用 modelsim 仿真调试。

第二种方法目前还不太稳定,如果遇到第二种方法烧录失败可以多烧录几次(可能因为接触不良),或者尝试一下第一种方法。

四、写在最后

至此,如何在本项目的 CPU 上运行 C 程序已经介绍完了,既然能运行 C 程序,那么运行一个实时操作系统应该也是没问题的,这也对应了本项目的 rt-thread 实验,大家感兴趣的话可以继续学习本仓库,后续也会出相应的文章!

如果遇到问题也欢迎加群 892873718 交流~ 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1185487.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【python海洋专题四十三】海洋指数画法--单色渐变柱状图

【python海洋专题四十三】海洋指数画法–单色渐变柱状图 【python海洋专题四十三】海洋指数画法–单色渐变柱状图 数据:AMO_index 数据:AMO_index 结果展示: 图片 往期推荐 图片 【python海洋专题一】查看数据nc文件的属性并输出属性到txt文件 【python海洋专题二…

C++学习---动态内存

文章目录 堆和栈new和delete操作符数组的动态内存分配对象的动态内存分配 堆和栈 在C中 栈&#xff1a;是一种静态内存分配区域&#xff0c;用于存储局部变量和函数调用的上下文信息。在栈上的内存分配和释放都是自动管理的&#xff0c;遵循后进先出&#xff08;LIFO&#xf…

从vue源码中看diff算法

一、v-for必须要指定key&#xff0c;其作用是什么&#xff1f; 在源码中有一个函数为&#xff0c;其中就是通过判断两个vnode的type和key进行判断&#xff0c;如果这两个属性相同&#xff0c;那么这两个vnode就是相同&#xff0c;所以在设置key的时候也不可以设置为object等无…

基于SSM的二手车交易网站的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

网工内推 | 上市公司,云平台运维,IP认证优先,13薪

01 上海新炬网络信息技术股份有限公司 招聘岗位&#xff1a;云平台运维工程师 职责描述&#xff1a; 1、负责云平台运维&#xff0c;包括例行巡检、版本发布、问题及故障处理、平台重保等&#xff0c;保障平台全年稳定运行&#xff1b; 2、参与制定运维标准规范与流程&#x…

【3D 图像分割】基于 Pytorch 的 VNet 3D 图像分割5(训练篇)

在本系列的开篇&#xff0c;就对整个项目训练所需要的所有模块都进行了一个简要的介绍&#xff0c;尤其是针对训练中需要引入的各个结构&#xff0c;进行一个串联操作。 而在之前的数据构建篇和网络模型篇中&#xff0c;都对其中的每一个组块进行了分别的验证&#xff0c;预先…

python3+requests接口自动化测试框架

前段时间由于公司测试方向的转型&#xff0c;由原来的web页面功能测试转变成接口测试&#xff0c;之前大多都是手工进行&#xff0c;利用postman和jmeter进行的接口测试&#xff0c;后来&#xff0c;组内有人讲原先web自动化的测试框架移驾成接口的自动化框架&#xff0c;使用的…

MCU常见通信总线串讲(四)—— SPI总线协议

&#x1f64c;秋名山码民的主页 &#x1f602;oi退役选手&#xff0c;Java、大数据、单片机、IoT均有所涉猎&#xff0c;热爱技术&#xff0c;技术无罪 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; 获取源码&#xff0c;添加WX 目录 前言一…

什么是API接口测试?这可能是全网最全的教程了!

什么是 API &#xff1f; API 是“应用程序编程接口”的缩写&#xff0c;是一种允许不同应用程序之间相互通信和交换数据的接口。就好像在餐厅点餐一样&#xff0c;你只需要告诉服务员你想要的食物&#xff0c;而不需要了解厨房中的具体操作&#xff0c;服务员会把你的订单传递…

深入asyncio:构建异步应用

文章目录 异步I/O操作示例:异步网络请求异步任务管理示例:并发执行多个任务使用异步队列示例:生产者-消费者模式在现代软件开发中,异步编程已经成为提高应用性能和响应性的关键技术之一。Python的asyncio库为编写单线程并发代码提供了强大的支持。本文将深入探讨asyncio的三…

CCF CSP认证 历年题目自练Day44

题目一 试题编号&#xff1a; 201612-3 试题名称&#xff1a; 权限查询 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 256.0MB 问题描述&#xff1a; 问题描述   授权 (authorization) 是各类业务系统不可缺少的组成部分&#xff0c;系统用户通过授权机制获得系统中各个…

python教程:把多张图片,合并成一张图

D:\Wdpython\environment\Scripts\python.exe D:/Wdpython/爬虫/测试8.py 图片列表 10 [‘刘亦菲/刘亦菲_1.jpg’, ‘刘亦菲/刘亦菲_11.jpg’, ‘刘亦菲/刘亦菲_12.jpg’, ‘刘亦菲/刘亦菲_13.jpg’, ‘刘亦菲/刘亦菲_15.jpg’, ‘刘亦菲/刘亦菲_2.jpg’, ‘刘亦菲/刘亦菲_3.jp…

MacOS升级后命令行出现xcrun: error: invalid active developer path报错信息

在Mac上用g编译cpp文件时&#xff0c;出现以下&#xff08;类似于工具环境问题的&#xff09;报错&#xff1a; 解决方案&#xff1a;重新安装最新版的MacOS Command Line Tools xcode-select --install重新尝试编译&#xff1a; 编译成功&#xff08;忽略这个warning&…

搭建自己的MQTT服务器,实现设备上云(Ubuntu+EMQX)

一、EMQX介绍 这篇文章教大家在ECS云服务器上部署EMQX,搭建自己私有的MQTT服务器,配置EMQX实现设备上云,设备数据转发,存储;服务器我采用的华为云的ECS服务器,系统选择Ubuntu系统。 Windows版本的看这里: https://blog.csdn.net/xiaolong1126626497/article/details/1…

蓝桥杯每日一题2023.11.8

题目描述 题目分析 对于输入的abc我们可以以a为年也可以以c为年&#xff0c;将abc,cab,cba这三种情况进行判断合法性即可&#xff0c;注意需要排序去重&#xff0c;所以考虑使用set 此处为纯模拟的写法&#xff0c;但使用循环代码会更加简洁。 方法一&#xff1a; #include&…

servelt中请求路径的组成部分(Request Path Elements)

详情请参考&#xff1a;https://jakarta.ee/specifications/servlet/6.0/jakarta-servlet-spec-6.0.html#request-path-elements 请求路径包含下面几部分&#xff1a; Context Path&#xff1a;与ServletContext 关联的路径前缀。如果这个上下文是默认的上下文&#xff08;以…

定位内存溢出错误

添加虚拟机参数 -XX:HeapDumpOnOutOfMemoryError&#xff08;内存溢出则会自动生成"溢出分析文件"&#xff0c;如图1&#xff09; 图1 -Xmx512m&#xff08;为了快速模拟内存溢出&#xff0c;暂设堆空间为512m&#xff09; 编写发生内存溢出代码 public class Me…

计算机基础知识48

web应用程序 # Django框架是一款专门用来开发web应用的框架 # Web应用程序是一种可以通过浏览器访问的应用程序, B/S架构 案例&#xff1a;淘宝网、京东网... # 应用程序有两种模式: C/S&#xff1a;客户端/服务器端程序&#xff0c;这类程序一般独立运行 B/S&#xff1…

子查询,内查询 嵌套查询

子查询语句 一个查询语句中还有一个查询语句 Select&#xff0c;&#xff0c;&#xff0c;&#xff08;select&#xff09; 括号里的查询语句优先于查询语句执行&#xff0c;然后再把子查询的结果作为条件返回给主查询条件进行过滤 格式&#xff1a; #In Select 列名from …