Linux x86_64平台指令替换函数 text_poke_smp/bp

文章目录

  • 前言
  • 一、text_poke_early
    • 1.1 text_poke_early简介
    • 1.2 用途
  • 二、text_poke_smp
    • 2.1 简介
      • 2.1.1 text_poke_smp函数
      • 2.2.2 stop_machine_text_poke简介
      • 2.2.3 text_poke函数
    • 2.2 用途
  • 三、text_poke_smp 内核hook

前言

Linux x86_64平台指令替换函数有两种类型:
(1)在内核启动阶段指令替换函数:text_poke_early
(2)在内核运行阶段指令替换函数:text_poke_smp/bp

在3.12.0版本以后,内核运行阶段指令替换函数由text_poke_smp改为text_poke_bp。

这里我以centos7 3.10.0为例主要介绍text_poke_smp函数。

一、text_poke_early

1.1 text_poke_early简介

// linux-3.10/arch/x86/kernel/alternative.c/*** text_poke_early - Update instructions on a live kernel at boot time* @addr: address to modify* @opcode: source of the copy* @len: length to copy** When you use this code to patch more than one byte of an instruction* you need to make sure that other CPUs cannot execute this code in parallel.* Also no thread must be currently preempted in the middle of these* instructions. And on the local CPU you need to be protected again NMI or MCE* handlers seeing an inconsistent instruction while you patch.*/
void *__init_or_module text_poke_early(void *addr, const void *opcode,size_t len)
{unsigned long flags;local_irq_save(flags);memcpy(addr, opcode, len);sync_core();local_irq_restore(flags);/* Could also do a CLFLUSH here to speed up CPU recovery; butthat causes hangs on some VIA CPUs. */return addr;
}

这段代码实现了在系统引导时(boot time)修改内核指令的函数text_poke_early()。它用于在引导过程中更新内核指令,以便在早期阶段进行修补。

函数的参数包括:

addr:要修改的指令的地址。
opcode:要复制的指令的源地址。
len:要复制的指令长度。

函数的实现步骤如下:
(1)使用local_irq_save()保存当前CPU的中断状态,并禁用中断。
(2)使用memcpy()将源指令(opcode)的内容复制到目标地址(addr)。
(3)调用sync_core()进行同步,以确保指令修改生效。
(4)使用local_irq_restore()恢复之前保存的中断状态,重新启用中断。

1.2 用途

主要用于以下几个地方:
(1)alternatives 指令替换:具体可以参考:Linux x86_64架构 动态替换 altinstructions

内核启动时进行alternatives 指令替换。

/* Replace instructions with better alternatives for this CPU type.This runs before SMP is initialized to avoid SMP problems withself modifying code. This implies that asymmetric systems whereAPs have less capabilities than the boot processor are not handled.Tough. Make sure you disable such features by hand. */void __init_or_module apply_alternatives(struct alt_instr *start,struct alt_instr *end)
{struct alt_instr *a;u8 *instr, *replacement;u8 insnbuf[MAX_PATCH_LEN];DPRINTK("%s: alt table %p -> %p\n", __func__, start, end);/** The scan order should be from start to end. A later scanned* alternative code can overwrite a previous scanned alternative code.* Some kernel functions (e.g. memcpy, memset, etc) use this order to* patch code.** So be careful if you want to change the scan order to any other* order.*/for (a = start; a < end; a++) {instr = (u8 *)&a->instr_offset + a->instr_offset;replacement = (u8 *)&a->repl_offset + a->repl_offset;BUG_ON(a->replacementlen > a->instrlen);BUG_ON(a->instrlen > sizeof(insnbuf));BUG_ON(a->cpuid >= (NCAPINTS + NBUGINTS) * 32);if (!boot_cpu_has(a->cpuid))continue;memcpy(insnbuf, replacement, a->replacementlen);/* 0xe8 is a relative jump; fix the offset. */if (*insnbuf == 0xe8 && a->replacementlen == 5)*(s32 *)(insnbuf + 1) += replacement - instr;add_nops(insnbuf + a->replacementlen,a->instrlen - a->replacementlen);text_poke_early(instr, insnbuf, a->instrlen);}
}

(2)Linux jump label机制:具体可参考:Linux Static Keys和jump label机制

内核启动时修改jump label。

// linux-3.10/init/main.c
start_kernel()-->jump_label_init()-->arch_jump_label_transform_static()
__init_or_module void arch_jump_label_transform_static(struct jump_entry *entry,enum jump_label_type type)
{__jump_label_transform(entry, type, text_poke_early);
}
static void __jump_label_transform(struct jump_entry *entry,enum jump_label_type type,void *(*poker)(void *, const void *, size_t))
{union jump_code_union code;if (type == JUMP_LABEL_ENABLE) {code.jump = 0xe9;code.offset = entry->target -(entry->code + JUMP_LABEL_NOP_SIZE);} elsememcpy(&code, ideal_nops[NOP_ATOMIC5], JUMP_LABEL_NOP_SIZE);(*poker)((void *)entry->code, &code, JUMP_LABEL_NOP_SIZE);
}

(3)Linux Static calls机制:Linux Static calls机制

// linux-5.15/arch/x86/kernel/static_call.cstatic void __ref __static_call_transform(void *insn, enum insn_type type, void *func)
{const void *emulate = NULL;int size = CALL_INSN_SIZE;const void *code;switch (type) {case CALL:code = text_gen_insn(CALL_INSN_OPCODE, insn, func);if (func == &__static_call_return0) {emulate = code;code = &xor5rax;}break;case NOP:code = x86_nops[5];break;case JMP:code = text_gen_insn(JMP32_INSN_OPCODE, insn, func);break;case RET:code = text_gen_insn(RET_INSN_OPCODE, insn, func);size = RET_INSN_SIZE;break;}if (memcmp(insn, code, size) == 0)return;if (unlikely(system_state == SYSTEM_BOOTING))return text_poke_early(insn, code, size);text_poke_bp(insn, code, size, emulate);
}

(4)ftrace

二、text_poke_smp

2.1 简介

2.1.1 text_poke_smp函数

/*** text_poke_smp - Update instructions on a live kernel on SMP* @addr: address to modify* @opcode: source of the copy* @len: length to copy** Modify multi-byte instruction by using stop_machine() on SMP. This allows* user to poke/set multi-byte text on SMP. Only non-NMI/MCE code modifying* should be allowed, since stop_machine() does _not_ protect code against* NMI and MCE.** Note: Must be called under get_online_cpus() and text_mutex.*/
void *__kprobes text_poke_smp(void *addr, const void *opcode, size_t len)
{struct text_poke_params tpp;struct text_poke_param p;p.addr = addr;p.opcode = opcode;p.len = len;tpp.params = &p;tpp.nparams = 1;atomic_set(&stop_machine_first, 1);wrote_text = 0;/* Use __stop_machine() because the caller already got online_cpus. */__stop_machine(stop_machine_text_poke, (void *)&tpp, cpu_online_mask);return addr;
}

这段代码实现了在SMP(对称多处理器)系统上更新内核中的指令的函数text_poke_smp()。它使用stop_machine()函数在SMP系统上修改多字节指令。这允许用户在SMP系统上修改多字节的内核指令。但需要注意的是,由于stop_machine()函数无法保护代码免受NMI(非屏蔽中断)和MCE(机器检查异常)的影响,因此只应允许非NMI和MCE的代码进行修改。

函数的参数包括:

addr:要修改的指令的地址。
opcode:要复制的指令的源地址。
len:要复制的指令长度。

主要调用stop_machine_text_poke函数。

2.2.2 stop_machine_text_poke简介

/** Cross-modifying kernel text with stop_machine().* This code originally comes from immediate value.*/
static atomic_t stop_machine_first;
static int wrote_text;struct text_poke_params {struct text_poke_param *params;int nparams;
};static int __kprobes stop_machine_text_poke(void *data)
{struct text_poke_params *tpp = data;struct text_poke_param *p;int i;if (atomic_xchg(&stop_machine_first, 0)) {for (i = 0; i < tpp->nparams; i++) {p = &tpp->params[i];text_poke(p->addr, p->opcode, p->len);}smp_wmb();	/* Make sure other cpus see that this has run */wrote_text = 1;} else {while (!wrote_text)cpu_relax();smp_mb();	/* Load wrote_text before following execution */}for (i = 0; i < tpp->nparams; i++) {p = &tpp->params[i];flush_icache_range((unsigned long)p->addr,(unsigned long)p->addr + p->len);}/** Intel Archiecture Software Developer's Manual section 7.1.3 specifies* that a core serializing instruction such as "cpuid" should be* executed on _each_ core before the new instruction is made visible.*/sync_core();return 0;
}

这段代码实现了使用stop_machine()函数进行内核文本交叉修改的功能。它通过stop_machine_text_poke()函数实现了在停机状态下修改内核文本的操作。

代码中使用了以下全局变量:

atomic_t类型的stop_machine_first:用于标记是否为第一次执行stop_machine_text_poke()函数。
int类型的wrote_text:表示是否已经修改了文本。

结构体text_poke_params定义了传递给stop_machine_text_poke()函数的参数:

params:指向text_poke_param结构体数组的指针,用于存储要修改的参数。
nparams:参数数组的长度。

stop_machine_text_poke()函数的实现步骤如下:
(1)通过传递的参数data获取text_poke_params结构体指针tpp。
(2)如果atomic_xchg()函数将stop_machine_first的值交换为0,表示当前是第一次执行stop_machine_text_poke()函数,则执行以下操作:
a:遍历tpp->params数组,依次获取text_poke_param结构体指针p。
b:调用text_poke()函数,将p->addr地址处的指令替换为p->opcode指令,长度为p->len。
c: 使用smp_wmb()确保其他CPU能够看到此次修改。
d:将wrote_text设置为1,表示已经修改了文本。
(3)否则,即不是第一次执行stop_machine_text_poke()函数,则进入循环,直到wrote_text变为非0。
(4)使用smp_mb()在执行后加载wrote_text之前进行内存屏障操作。
(5)遍历tpp->params数组,依次获取text_poke_param结构体指针p。
(6)调用flush_icache_range()函数,刷新从p->addr到(p->addr + p->len)范围内的指令缓存。
(7)使用sync_core()函数进行核心同步,确保新指令在每个核心上都可见。

2.2.3 text_poke函数

/*** text_poke - Update instructions on a live kernel* @addr: address to modify* @opcode: source of the copy* @len: length to copy** Only atomic text poke/set should be allowed when not doing early patching.* It means the size must be writable atomically and the address must be aligned* in a way that permits an atomic write. It also makes sure we fit on a single* page.** Note: Must be called under text_mutex.*/
void *__kprobes text_poke(void *addr, const void *opcode, size_t len)
{unsigned long flags;char *vaddr;struct page *pages[2];int i;if (!core_kernel_text((unsigned long)addr)) {pages[0] = vmalloc_to_page(addr);pages[1] = vmalloc_to_page(addr + PAGE_SIZE);} else {pages[0] = virt_to_page(addr);WARN_ON(!PageReserved(pages[0]));pages[1] = virt_to_page(addr + PAGE_SIZE);}BUG_ON(!pages[0]);local_irq_save(flags);set_fixmap(FIX_TEXT_POKE0, page_to_phys(pages[0]));if (pages[1])set_fixmap(FIX_TEXT_POKE1, page_to_phys(pages[1]));vaddr = (char *)fix_to_virt(FIX_TEXT_POKE0);memcpy(&vaddr[(unsigned long)addr & ~PAGE_MASK], opcode, len);clear_fixmap(FIX_TEXT_POKE0);if (pages[1])clear_fixmap(FIX_TEXT_POKE1);local_flush_tlb();sync_core();/* Could also do a CLFLUSH here to speed up CPU recovery; butthat causes hangs on some VIA CPUs. */for (i = 0; i < len; i++)BUG_ON(((char *)addr)[i] != ((char *)opcode)[i]);local_irq_restore(flags);return addr;
}

这段代码实现了在实时内核上更新指令的函数text_poke()。它用于在实时内核中修改指定地址的指令。

函数的参数包括:

addr:要修改的指令的地址。
opcode:要复制的指令的源地址。
len:要复制的指令的长度。

函数的实现步骤如下:
(1)根据地址addr的特性,判断是否位于核心内核文本区域。如果不是核心内核文本区域,使用vmalloc_to_page()将地址转换为对应的内存页,并存储在pages数组中的第一个位置和第二个位置。
(2)如果地址位于核心内核文本区域,使用virt_to_page()将地址转换为对应的内存页,并存储在pages数组中的第一个位置。同时,使用WARN_ON()检查该页是否为保留页。
(3)使用BUG_ON()检查pages[0]是否为空。
(4)使用local_irq_save()保存当前中断状态,并禁用中断。
(5)使用set_fixmap()将pages[0]与FIX_TEXT_POKE0进行映射,将其物理地址设置为页表项的值。
(6)如果pages[1]不为空,使用set_fixmap()将pages[1]与FIX_TEXT_POKE1进行映射,将其物理地址设置为页表项的值。
(7)将fix_to_virt()的结果赋给vaddr,即获取FIX_TEXT_POKE0映射的虚拟地址。
(8)使用memcpy()将opcode的内容复制到vaddr和addr在页内的相对偏移位置上,长度为len。
(9)使用clear_fixmap()清除FIX_TEXT_POKE0的映射。
(10)如果pages[1]不为空,使用clear_fixmap()清除FIX_TEXT_POKE1的映射。
(11)使用local_flush_tlb()刷新TLB(转换后备缓冲)以确保新的页表项生效。
(12)使用sync_core()进行核心同步,确保新指令在每个核心上都可见。
(13)使用一个循环遍历检查修改后的指令是否和原始指令一致。如果不一致,使用BUG_ON()触发错误。
(14)使用local_irq_restore()恢复之前保存的中断状态。
(15)返回被修改的地址。

该函数用于在实时内核中修改指令。它将指定地址的指令替换为新的指令,并进行了一系列的同步操作和错误检查,以确保修改的指令能够正确执行。需要注意的是,在调用text_poke()函数之前,必须在text_mutex的保护下进行,以确保互斥访问。

2.2 用途

(1)Linux jump label机制:具体可参考:Linux Static Keys和jump label机制

内核运行时修改jump label。

static_key_slow_inc/dec-->jump_label_update()-->__jump_label_update()-->arch_jump_label_transform()
void arch_jump_label_transform(struct jump_entry *entry,enum jump_label_type type)
{get_online_cpus();mutex_lock(&text_mutex);__jump_label_transform(entry, type, text_poke_smp);mutex_unlock(&text_mutex);put_online_cpus();
}

(2)kprobe机制:优化kprobe,使用jmp指令替换int3指令。

/** Replace breakpoints (int3) with relative jumps.* Caller must call with locking kprobe_mutex and text_mutex.*/
void __kprobes arch_optimize_kprobes(struct list_head *oplist)
{struct optimized_kprobe *op, *tmp;int c = 0;list_for_each_entry_safe(op, tmp, oplist, list) {WARN_ON(kprobe_disabled(&op->kp));/* Setup param */setup_optimize_kprobe(&jump_poke_params[c],jump_poke_bufs[c].buf, op);list_del_init(&op->list);if (++c >= MAX_OPTIMIZE_PROBES)break;}/** text_poke_smp doesn't support NMI/MCE code modifying.* However, since kprobes itself also doesn't support NMI/MCE* code probing, it's not a problem.*/text_poke_smp_batch(jump_poke_params, c);
}

(3)Linux Static calls机制:Linux Static calls机制

// linux-5.15/arch/x86/kernel/static_call.cstatic void __ref __static_call_transform(void *insn, enum insn_type type, void *func)
{const void *emulate = NULL;int size = CALL_INSN_SIZE;const void *code;switch (type) {case CALL:code = text_gen_insn(CALL_INSN_OPCODE, insn, func);if (func == &__static_call_return0) {emulate = code;code = &xor5rax;}break;case NOP:code = x86_nops[5];break;case JMP:code = text_gen_insn(JMP32_INSN_OPCODE, insn, func);break;case RET:code = text_gen_insn(RET_INSN_OPCODE, insn, func);size = RET_INSN_SIZE;break;}if (memcmp(insn, code, size) == 0)return;if (unlikely(system_state == SYSTEM_BOOTING))return text_poke_early(insn, code, size);text_poke_bp(insn, code, size, emulate);
}

(4)ftrace

三、text_poke_smp 内核hook

text_poke_smp 可以在内核运行时修改内存指令,因此可以用来进行内核hook。

例子请参考:
https://blog.csdn.net/bin_linux96/article/details/105776231
https://blog.csdn.net/dog250/article/details/105254739
https://blog.csdn.net/dog250/article/details/105787199
https://blog.csdn.net/dog250/article/details/84201114
https://blog.csdn.net/qq_21792169/article/details/84583275
https://richardweiyang-2.gitbook.io/kernel-exploring/00-index/04-ftrace_internal

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

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

相关文章

万界星空科技机械加工行业MES解决方案

机械加工行业作为制造业的重要组成部分&#xff0c;面临着生产效率、成本控制和产品质量提升等多重挑战。为了应对这些挑战&#xff0c;引入并实施制造执行系统&#xff08;MES&#xff09;成为了行业的必然选择。本文将详细介绍一种针对机械加工行业的MES解决方案&#xff0c;…

【Docekr】容器自动重启/取消自动重启

Docekr 容器自动重启/取消自动重启 文章目录 Docekr 容器自动重启/取消自动重启Docker 重启模式Docker 更新容器 Docker 重启模式 Docker的restart可以控制容器的重启模式&#xff0c;其有以下几个值&#xff1a; # 默认策略&#xff0c;容器退出时不重启容器 --restartno# 在…

用MySQL+node+vue做一个学生信息管理系统(二):创建MySQL数据表、创建HTML用户列表页面

MySQL代码 CREATE DATABASE students;USE students;CREATE TABLE student( id INT COMMENT 学号, name VARCHAR(32) COMMENT 姓名, sex VARCHAR(8) COMMENT 性别, class VARCHAR(64) COMMENT 班级 )SHOW TABLES;下面介绍一下Vue框架的element-ui的使用方法&#xff0c;这里就不…

常用排序算法_06_归并排序

1、基本思想 归并排序采用分治法 (Divide and Conquer) 的一个非常典型的应。归并排序的思想就是先递归分解数组&#xff0c;再合并数组。归并排序是一种稳定的排序方法。 将数组分解最小之后&#xff08;数组中只有一个元素&#xff0c;数组有序&#xff09;&#xff1b;然后…

Spring Boot + liteflow 居然这么好用!实战

在我们的日常开发中&#xff0c;经常会遇到一些需要串行或并行处理的复杂业务流程。 那我们该如何利用Spring Boot结合liteflow规则引擎来简化我们的业务流程 先看一个实战案例&#xff01;&#xff01; 在电商场景下&#xff0c;当订单完成后&#xff0c;我们需要同时进行积…

go 为什么是抢占式调度

GMP 模型 gmp模型是 golang 中用于调度管理 goroutine 的调度器。 调度器的发展史 在 Go 语言中&#xff0c;Goroutine 早期是没有设计成抢占式的&#xff0c;早期 Goroutine 只有读写、主动让出、锁等操作时才会触发调度切换。 这样有一个严重的问题&#xff0c;就是垃圾回…

鸿蒙笔记导航栏,路由,还有axios

1.导航组件 导航栏位置可以调整&#xff0c;导航栏位置 Entry Component struct t1 {build() {Tabs(){TabContent() {Text(qwer)}.tabBar("首页")TabContent() {Text(发现内容)}.tabBar(发现)TabContent() {Text(我的内容)}.tabBar("我的")}// 做平板适配…

文心一言:探索AI写作的新境界

在人工智能飞速发展的今天&#xff0c;AI写作助手已经成为许多写作者、内容创作者和营销专家的重要工具。"文心一言"作为一个先进的AI写作平台&#xff0c;以其强大的语言理解和生成能力&#xff0c;为用户提供了从文本生成到编辑、优化等一系列服务。本文将介绍如何…

【Springer出版 | EI稳定检索】第五届物联网、人工智能与机械自动化国际学术会议 (IoTAIMA 2024,7月19-21)

由浙江工业大学主办&#xff0c;第五届物联网、人工智能与机械自动化国际学术会议 (IoTAIMA 2024) 将于2024年7月19-21日在浙江杭州召开。 会议旨在为从事物联网、人工智能与机械自动化的专家学者、工程技术人员、技术研发人员提供一个共享科研成果和前沿技术&#xff0c;了解学…

深入Django(七)

Django的数据库迁移系统 引言 在前六天的教程中&#xff0c;我们介绍了Django的基本概念、模型、视图、模板、URL路由和表单系统。今天&#xff0c;我们将讨论Django的数据库迁移系统&#xff0c;它是管理和跟踪数据库变化的关键组件。 Django数据库迁移概述 Django的数据库…

基于java+springboot+vue实现的药店管理系统(文末源码+Lw)285

摘 要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;药品信息因为其管理内容繁杂&#xff0c;管理数量繁多导致手工进行处理不能满足广…

论文创新的几种思路

选题是论文创新的基石&#xff0c;它决定了研究的方向和深度。一个新颖的选题不仅能够吸引读者和评审的注意&#xff0c;还能为学术界带来新的视角和思考。选题创新要求研究者具有敏锐的洞察力&#xff0c;能够识别和捕捉到那些尚未被充分探索或有待深入研究的问题。 选题创新…

SSM家庭理财个人理财系统-JAVA【数据库设计、源码、开题报告】

第一章 绪论 1.1 课题背景、目的及意义 从 20 世纪末以来&#xff0c;在全球经济日趋一体化的背景之下&#xff0c;中国经济也得到了飞速的发展&#xff0c;家庭收入也快速增长。居民的消费结构发生了巨大变化&#xff0c;购置房产、旅游、汽车消费、教育等成为居民消费重点。…

一维前缀和的实现

这是C算法基础-基础算法专栏的第十一篇文章&#xff0c;专栏详情请见此处。 引入 我们用朴素做法求一维数组的区间和时&#xff0c;一般是从前向后循环累加&#xff0c;它的时间复杂度为&#xff0c;当求区间和的次数过多&#xff0c;则会有超时的可能&#xff0c;那有没有时间…

算法设计练笔

T1、给定由n个整数&#xff08;可能为负数&#xff09;组成的序列a1,a2,...,an&#xff0c;求该序列子段和的最大值。当所有整数均为负数时&#xff0c;其最大子段和为0。 【输入】 第一行一个整数&#xff0c;表示n的值, 1<n<100; 第二行n个整数&#xff0c;表示a1,a2,.…

ubuntu 查看联网配置

在Ubuntu中&#xff0c;你可以使用多种命令来查看联网配置。以下是一些常用的方法和命令&#xff1a; 查看网络接口配置&#xff1a; 使用 ip 命令可以查看网络接口的配置信息&#xff0c;包括IP地址、子网掩码等。 ip addr show或者&#xff0c;你也可以使用传统的 ifconfig 命…

【Unity URP】通过代码动态添加URP渲染通道RendererFeature

URP的渲染通道RendererFeature可以很方便的实现一些渲染问题,比如渲染顺序问题,遮挡后的材质替换等等。 那么我们如何通过代码来动态添加和修改呢? 首先我们需要获取到当前的URP配置文件,在对配置文件进行添加 1.通过反射获取当前UniversalRendererData 我们通过Graphic…

如何快速上手文心一言指令

快速上手文心一言指令&#xff0c;可以遵循以下步骤和要点&#xff0c;以确保高效且准确地与文心一言进行交互&#xff1a; 一、了解文心一言基础 1. 文心一言简介 文心一言是百度研发的人工智能模型&#xff0c;用户可以通过输入指令与其进行互动&#xff0c;提出问题或要求…

中国移动中国联通中国电信数字化转型营销销售讲师培训老师讲授AIGC大模型人工智能5G云算力网络云网终端AIGC人工智能宽带政企物联网专线 IDC智慧城市

唐兴通 数字化商业创新顾问、新媒体营销专家、数字化销售增长教练、沃顿商学院演讲嘉宾。全球创新增长战略大家EM罗杰斯&#xff08;创新的扩散&#xff09;、杰弗里摩尔&#xff08;跨越鸿沟&#xff09;、亨利切萨布鲁夫&#xff08;开放式创新&#xff09;在中国合作者。《…

gitee代码初次上传步骤

ps. 前提是已经下载安装gitee 一、在本地项目目录下空白处右击&#xff0c;选择“Git Bash Here” 二、初始化 git init 三、添加、提交代码&#xff08;注意add与点之间的空格&#xff09; git add . git commit -m 添加注释 四、连接、推送到gitee仓库 git remote add …