AM335x U-boot d代码分析过程1

2024-03-19 12:58
文章标签 分析 代码 过程 boot am335x

本文主要是介绍AM335x U-boot d代码分析过程1,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

题外话:

经过一段时间的学习,对u-boot-2014.10有了初步的了解,趁着还记着,赶紧写下来,同时将之前还模棱两可的部分用图表的方式加强一下。

源码分析

汇编部分

之前一直看的是ARM9的u-boot,AM335X系列为代表的ARMv7系列处理器为了使得加载方式尽可能的灵活,就将原来的u-boot分解成了两个部分:SPL和u-boot,而实际上在重启后,到SPL被执行之间,还有一段片上ROM内的代码被运行。该ROM内的代码根据启动时的配置引脚的配置,自动生成一个加载优先列表,比如说首先加载MMC1,在考虑MMC0,最后再考虑uart等等。比如说用户正确的将SPL(SPL加上一个包头变成MLO)和u-boot放置到SD中后,那么ROM就会在启动:1.按照加载列表的boot优先级逐个尝试,2.判断是MMC1上拥有有效设备(比如说SD卡已经插入)后,继续判断MMC上挂的是SD卡还是MMC内存,3. 查看格式 4. 加载SPL(MLO)到片内的SRAM, 5.cpu将pc指到SRAM,SPL正式开始运行。

下面开始分析,SPL是如何工作的,首先是程序的入口,在arch/arm/cpu/armv7/start.S中的程序的入口,reset,下面开始分析,源码如下:

[plain] view plain copy
print ?
  1. reset:  
  2.     bl  save_boot_params /*lowlevel_init.S (arch\arm\cpu\armv7\omap-common)*/  
  3.     /*  
  4.      * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,  
  5.      * except if in HYP mode already  
  6.      */  
  7.     mrs r0, cpsr  
  8.     and r1, r0, #0x1f       @ mask mode bits  
  9.     teq r1, #0x1a       @ test for HYP mode  
  10.     bicne   r0, r0, #0x1f       @ clear all mode bits  
  11.     orrne   r0, r0, #0x13       @ set SVC mode  
  12.     orr r0, r0, #0xc0       @ disable FIQ and IRQ  
  13.     msr cpsr,r0  
  14.   
  15. /*  
  16.  * Setup vector:  
  17.  * (OMAP4 spl TEXT_BASE is not 32 byte aligned.  
  18.  * Continue to use ROM code vector only in OMAP4 spl)  
  19.  */  
  20. #if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))  
  21.     /* Set V=0 in CP15 SCTRL register - for VBAR to point to vector */  
  22.     mrc p15, 0, r0, c1, c0, 0   @ Read CP15 SCTRL Register  
  23.     bic r0, #CR_V       @ V = 0  
  24.     mcr p15, 0, r0, c1, c0, 0   @ Write CP15 SCTRL Register  
  25.   
  26.     /* Set vector address in CP15 VBAR register */  
  27.     ldr r0, =_start  
  28.     mcr p15, 0, r0, c12, c0, 0  @Set VBAR  
  29. #endif  
  30.   
  31.     /* the mask ROM code should have PLL and others stable */  
  32. #ifndef CONFIG_SKIP_LOWLEVEL_INIT/*this branch will only work in SPL*/  
  33.     bl  cpu_init_cp15 /*wlg: find out in this file, we do not explain in detial*/  
  34.     bl  cpu_init_crit /*wlg: find out in this file, please jump*/  
  35. #endif  
reset:bl  save_boot_params /*lowlevel_init.S (arch\arm\cpu\armv7\omap-common)*//** disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,* except if in HYP mode already*/mrs r0, cpsrand r1, r0, #0x1f       @ mask mode bitsteq r1, #0x1a       @ test for HYP modebicne   r0, r0, #0x1f       @ clear all mode bitsorrne   r0, r0, #0x13       @ set SVC modeorr r0, r0, #0xc0       @ disable FIQ and IRQmsr cpsr,r0/** Setup vector:* (OMAP4 spl TEXT_BASE is not 32 byte aligned.* Continue to use ROM code vector only in OMAP4 spl)*/
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD)) /* Set V=0 in CP15 SCTRL register - for VBAR to point to vector */ mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTRL Register bic r0, #CR_V @ V = 0 mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTRL Register /* Set vector address in CP15 VBAR register */ ldr r0, =_start mcr p15, 0, r0, c12, c0, 0 @Set VBAR #endif /* the mask ROM code should have PLL and others stable */ #ifndef CONFIG_SKIP_LOWLEVEL_INIT/*this branch will only work in SPL*/ bl cpu_init_cp15 /*wlg: find out in this file, we do not explain in detial*/ bl cpu_init_crit /*wlg: find out in this file, please jump*/ #endif

    可以看到reset进去后,1. 首先需要执行save_boot_paras,它的入口就在lowlevel_init.S (arch\arm\cpu\armv7\omap-common)中,它的具体形式如下

[plain] view plain copy
print ?
  1. ENTRY(save_boot_params)  
  2.     ldr r1, =OMAP_SRAM_SCRATCH_BOOT_PARAMS  
  3.     str r0, [r1]  
  4.     bx  lr  
  5. ENDPROC(save_boot_params)  
ENTRY(save_boot_params)ldr r1, =OMAP_SRAM_SCRATCH_BOOT_PARAMSstr r0, [r1]bx  lr
ENDPROC(save_boot_params)
    可以看到,这里要做的工作实际上就是将r0里的数据复制到OMAP_SRAM_SCRATCH_BOOT_PARAMS这个位置,这个我会在第二篇中详细介绍。执行完了以后又跳回到start.S,继续往下看

    2.下面的代码请结合相应的书册,其原始注释已经写得很清楚了,一路往下走,来到了#ifndef CONFIG_SKIP_LOWLEVEL_INIT,这个东西我们简单的介绍下,像这种大的开源工程,其包含了很多的宏开关,相当于用户实际需需要什么,定义什么,就会有相应的驱动代码与之匹配。就像此处,实际上u-boot和SPL是共享start.S的,而实际上他们执行的东西是不一样的,如何让编译器正确的理解用户实际想要包含的代码段,一方面靠的是宏做条件编译,另一方面靠的是makefile,后者就更加复杂了;在这里我们分析的就是SPL程序,所以我们会在相应的文件中定义CONFIG_SKIP_LOWLEVEL_INIT这个宏,那么这段代码实际上就被包含在了SPL程序中;继续往下看:

    3. 跳到cpu_init_cp15,这段代码就在本文中,不再展开

    4.执行bl cpu_init_crit ,这个程序的入口就在本文档中,以下展开讨论:


[plain] view plain copy
print ?
  1. #ifndef CONFIG_SKIP_LOWLEVEL_INIT  
  2. /*************************************************************************  
  3.  *  
  4.  * CPU_init_critical registers  
  5.  *  
  6.  * setup important registers  
  7.  * setup memory timing  
  8.  *  
  9.  *************************************************************************/  
  10. ENTRY(cpu_init_crit)  
  11.     /*  
  12.      * Jump to board specific initialization…  
  13.      * The Mask ROM will have already initialized  
  14.      * basic memory. Go here to bump up clock rate and handle  
  15.      * wake up conditions.  
  16.      */  
  17.     b   lowlevel_init       @ go setup pll,mux,memory/*wlg: jump to /arch/arm/cpu/armv7/lowlevel_init.s*/  
  18. ENDPROC(cpu_init_crit)  
  19. #endif  
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
/*************************************************************************** CPU_init_critical registers** setup important registers* setup memory timing**************************************************************************/
ENTRY(cpu_init_crit)/** Jump to board specific initialization...* The Mask ROM will have already initialized* basic memory. Go here to bump up clock rate and handle* wake up conditions.*/b   lowlevel_init       @ go setup pll,mux,memory/*wlg: jump to /arch/arm/cpu/armv7/lowlevel_init.s*/
ENDPROC(cpu_init_crit)
#endif

    首先还是宏打头,表示条件编译,在SPL程序中,下面的代码段实际被包含!看注释,已经大致了解它的作用,整个代码段只有一句,直接转跳到下一个程序的入口,在/arch/arm/cpu/armv7/lowlevel_init.s中定义了lowlevel_init。注意到,之前转跳都是用的bl,表示会返回,但是这里却用的是b?我个人的理解是bl没法子应对二次转跳,也就是说在lowlevel_init中还会做更复杂的转跳,甚至开始进入C函数中,那么bl我觉得是无力的。那么这里到底需不需要返回呢?

    答案是需要的,但是会利用stack进行返回指针保存,利用push和pop来实现。 拿下一个问题就是,我们还没有初始化sp呢,你就敢乱用push?嘿嘿,我们继续往下看:

[plain] view plain copy
print ?
  1. ENTRY(lowlevel_init)  
  2.     /*  
  3.      * Setup a temporary stack  wlg: we set a temp stack in SRAM for building  
  4.                                 a enveriment for C program  */  
  5.     ldr sp, =CONFIG_SYS_INIT_SP_ADDR    /*wlg: this is a temp stack built in SRAM  
  6.     bic sp, sp, #7 /* 8-byte alignment for ABI compliance */  
  7. #ifdef CONFIG_SPL_BUILD  
  8.     ldr r9, =gdata  /*wlg: we set a temp gdata(global_data) in SRAM(.data section), and  */  
  9. #else                   /*wlg: r9 is point to that region in SRAM on chip ///SPL*/  
  10.     sub sp, sp, #GD_SIZE  
  11.     bic sp, sp, #7  /*wlg: cause uboot will define skip_lowlevel_init, so this branch will*/  
  12.     mov r9, sp          /*wlg: not active, even on stage of uboot!uboot*/  
  13. #endif  
  14.     /*  
  15.      * Save the old lr(passed in ip) and the current lr to stack  
  16.      */  
  17.     push    {ip, lr}    /*wlg: save the address return*/  
  18.   
  19.   
  20.     /*  
  21.      * go setup pll, mux, memory  
  22.      */  
  23.     bl  s_init          /*wlg: jump to arch/arm/cpu/armv7/am335x/board.c- s_init()*/  
  24.     pop {ip, pc}    /*wlg: to make SDRAM initialition, then make a return to _reset*/  
  25. ENDPROC(lowlevel_init)  
ENTRY(lowlevel_init)/** Setup a temporary stack  wlg: we set a temp stack in SRAM for buildinga enveriment for C program  */ldr sp, =CONFIG_SYS_INIT_SP_ADDR    /*wlg: this is a temp stack built in SRAMbic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#ifdef CONFIG_SPL_BUILD ldr r9, =gdata /*wlg: we set a temp gdata(global_data) in SRAM(.data section), and */ #else /*wlg: r9 is point to that region in SRAM on chip ///SPL*/ sub sp, sp, #GD_SIZE bic sp, sp, #7 /*wlg: cause uboot will define skip_lowlevel_init, so this branch will*/ mov r9, sp /*wlg: not active, even on stage of uboot!uboot*/ #endif /* * Save the old lr(passed in ip) and the current lr to stack */ push {ip, lr} /*wlg: save the address return*/ /* * go setup pll, mux, memory */ bl s_init /*wlg: jump to arch/arm/cpu/armv7/am335x/board.c- s_init()*/ pop {ip, pc} /*wlg: to make SDRAM initialition, then make a return to _reset*/ ENDPROC(lowlevel_init)

    1. 一开始就把sp指向了SRAM的高位CONFIG_SYS_INIT_SP_ADDR(因为低位拿来保存text程序了,高位用来保存数据bss,stack等等)

    2.做了对齐,下面又是一个条件编译,这里我们仍然执行的是ldr r9, =gdata;这个我们要好好分析一下,去寻找gdata的定义,会发现在某个c文件中定义了一个全局变量,即global_data的结构体数据,那么在实际运行的时候,gdata是在SRAM中的。所以这里仅需知道,用户将sp和r9都指向了SRAM中某处。大家都知道sp是专门拿来做堆栈的,那么r9在这里有什么用处呢?我们简单的介绍下,这里的r9,和后面会出面的gd是同一个东西,也就是说,当用用需要修改或读取gdata里的元素的数据时,直接可以使用gd.a,gd.b即可,因为r9实际指向了gdata,而gd实际上就是r9的一个别名(宏定义);

    3.再往下push {ip, lr},就是之前所说的用stack的方式保存lr(返回指针),所以我们在前面给sp赋值;同时,给sp赋值也相当于是为进入C语言函数做准备

    4. bl s_init,再次作了转跳,这是一个重点的部分,函数定义在arch/arm/cpu/armv7/am335x/board.c- s_init()

C语言

实际上在之前已经完成了sp的初始化,我们可以开始调用C语言函数了。同时,我们需要看到在文档board.c的最下方
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm (“r9”)
这实际上就是说,以后用户调用gd的地方,实际上就是用了r9里的数作为指针,而经过上面的分析,r9实际上指向了gdata,用户定义的一个global_data的结构体。一句话说就是,gd不是一个野指针,而是实际上指向了gdata;以后不在展开讨论;
继续往下看,首先是一个条件编译,这部分我觉得是跳过的,也就是说没有被编译进来,继续往下看
进入了save_omap_boot_params()函数,该函数在arch/arm/cpu/armv7/Omap-common/Boot-common.c中定义;我们之前提及过boot参数,之前一直没有保存,就是因为当时才启动,没有合适的保存boot参数的区域(gd指针还没有实际指向有效的数据区域)。当用户完成所有的准备工作后,那么boot参数的保存就必须要开始了:

[cpp] view plain copy
print ?
  1. void s_init(void)  
  2. {  
  3.     /* 
  4.      * The ROM will only have set up sufficient pinmux to allow for the 
  5.      * first 4KiB NOR to be read, we must finish doing what we know of 
  6.      * the NOR mux in this space in order to continue. 
  7.      */  
  8. #ifdef CONFIG_NOR_BOOT  
  9.     enable_norboot_pin_mux();  
  10. #endif  
  11.     /* 
  12.      * Save the boot parameters passed from romcode. 
  13.      * We cannot delay the saving further than this, 
  14.      * to prevent overwrites. 
  15.      */  
  16. #ifdef CONFIG_SPL_BUILD  
  17.     save_omap_boot_params();//wlg:  should record the boot parament now, arch/arm/cpu/armv7/Omap-common/Boot-common.c  
void s_init(void)
{/** The ROM will only have set up sufficient pinmux to allow for the* first 4KiB NOR to be read, we must finish doing what we know of* the NOR mux in this space in order to continue.*/
#ifdef CONFIG_NOR_BOOT enable_norboot_pin_mux(); #endif /* * Save the boot parameters passed from romcode. * We cannot delay the saving further than this, * to prevent overwrites. */ #ifdef CONFIG_SPL_BUILD save_omap_boot_params();//wlg: should record the boot parament now, arch/arm/cpu/armv7/Omap-common/Boot-common.c


[cpp] view plain copy
print ?
  1. void save_omap_boot_params(void)  
  2. {  
  3.     u32 rom_params = *((u32 *)OMAP_SRAM_SCRATCH_BOOT_PARAMS);//wlg: look in TRM P4954  
  4.     u8 boot_device;//wlg:   
  5.     u32 dev_desc, dev_data;  
  6.   
  7.     if ((rom_params <  NON_SECURE_SRAM_START) ||  
  8.         (rom_params > NON_SECURE_SRAM_END))  
  9.         return;  
  10.   
  11.     /* 
  12.      * rom_params can be type casted to omap_boot_parameters and 
  13.      * used. But it not correct to assume that romcode structure 
  14.      * encoding would be same as u-boot. So use the defined offsets. 
  15.      */  
  16.     gd->arch.omap_boot_params.omap_bootdevice = boot_device =  
  17.                    *((u8 *)(rom_params + BOOT_DEVICE_OFFSET));//wlg: P4954, offset = 0x8,  
  18.                     //wlg: it point to Current Booting Device!the parament will be use to copy uboot  
  19.     gd->arch.omap_boot_params.ch_flags =  
  20.                 *((u8 *)(rom_params + CH_FLAGS_OFFSET));  
  21.   
  22.     if ((boot_device >= MMC_BOOT_DEVICES_START) &&//wlg: boot device list from 0-6,XIP,MMC0,MMC1 and so on  
  23.         (boot_device <= MMC_BOOT_DEVICES_END)) {  
  24. #if !defined(CONFIG_AM33XX) && !defined(CONFIG_TI81XX) && \//wlg: skip this   
  25.     !defined(CONFIG_AM43XX)  
  26.         if ((omap_hw_init_context() ==  
  27.                       OMAP_INIT_CONTEXT_UBOOT_AFTER_SPL)) {  
  28.             gd->arch.omap_boot_params.omap_bootmode =  
  29.             *((u8 *)(rom_params + BOOT_MODE_OFFSET));  
  30.         } else  
  31. #endif  
  32.         {  
  33.             dev_desc = *((u32 *)(rom_params + DEV_DESC_PTR_OFFSET));//wlg: point to memory device descriptor  
  34.             dev_data = *((u32 *)(dev_desc + DEV_DATA_PTR_OFFSET));  
  35.             gd->arch.omap_boot_params.omap_bootmode =  
  36.                     *((u32 *)(dev_data + BOOT_MODE_OFFSET));//wlg: record the boot mode into global_data(temp)  
  37.         }  
  38.     }  
  39.   
  40. #ifdef CONFIG_DRA7XX  
  41.     /* 
  42.      * We get different values for QSPI_1 and QSPI_4 being used, but 
  43.      * don’t actually care about this difference.  Rather than 
  44.      * mangle the later code, if we’re coming in as QSPI_4 just 
  45.      * change to the QSPI_1 value. 
  46.      */  
  47.     if (gd->arch.omap_boot_params.omap_bootdevice == 11)  
  48.         gd->arch.omap_boot_params.omap_bootdevice = BOOT_DEVICE_SPI;  
  49. #endif//wlg: return to s_init()   
  50. }  
void save_omap_boot_params(void)
{u32 rom_params = *((u32 *)OMAP_SRAM_SCRATCH_BOOT_PARAMS);//wlg: look in TRM P4954u8 boot_device;//wlg: u32 dev_desc, dev_data;if ((rom_params <  NON_SECURE_SRAM_START) ||(rom_params > NON_SECURE_SRAM_END))return;/** rom_params can be type casted to omap_boot_parameters and* used. But it not correct to assume that romcode structure* encoding would be same as u-boot. So use the defined offsets.*/gd->arch.omap_boot_params.omap_bootdevice = boot_device =*((u8 *)(rom_params + BOOT_DEVICE_OFFSET));//wlg: P4954, offset = 0x8,//wlg: it point to Current Booting Device!the parament will be use to copy ubootgd->arch.omap_boot_params.ch_flags =*((u8 *)(rom_params + CH_FLAGS_OFFSET));if ((boot_device >= MMC_BOOT_DEVICES_START) &&//wlg: boot device list from 0-6,XIP,MMC0,MMC1 and so on(boot_device <= MMC_BOOT_DEVICES_END)) {

if !defined(CONFIG_AM33XX) && !defined(CONFIG_TI81XX) && \//wlg: skip this

!defined(CONFIG_AM43XX)if ((omap_hw_init_context() ==OMAP_INIT_CONTEXT_UBOOT_AFTER_SPL)) {gd-&gt;arch.omap_boot_params.omap_bootmode =*((u8 *)(rom_params + BOOT_MODE_OFFSET));} else

endif

    {dev_desc = *((u32 *)(rom_params + DEV_DESC_PTR_OFFSET));//wlg: point to memory device descriptordev_data = *((u32 *)(dev_desc + DEV_DATA_PTR_OFFSET));gd-&gt;arch.omap_boot_params.omap_bootmode =*((u32 *)(dev_data + BOOT_MODE_OFFSET));//wlg: record the boot mode into global_data(temp)}
}

ifdef CONFIG_DRA7XX

/** We get different values for QSPI_1 and QSPI_4 being used, but* don't actually care about this difference.  Rather than* mangle the later code, if we're coming in as QSPI_4 just* change to the QSPI_1 value.*/
if (gd-&gt;arch.omap_boot_params.omap_bootdevice == 11)gd-&gt;arch.omap_boot_params.omap_bootdevice = BOOT_DEVICE_SPI;

endif//wlg: return to s_init()

}

    boot参数请查看TRM P4954,也就是技术文档的4954页,里面记录了参数保存的顺序以及名称,将其用指针(rom_params)取出后一次放入到gd所指向的gdata中,一般有:

    1.boot原因

    2.SPL文件来源等等

    这些参数在后期启动uboot都有作用,这里不展开先,过几天在续写一下!2017.01.22夜

这篇关于AM335x U-boot d代码分析过程1的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/826068

相关文章

Spring Boot @RestControllerAdvice全局异常处理最佳实践

《SpringBoot@RestControllerAdvice全局异常处理最佳实践》本文详解SpringBoot中通过@RestControllerAdvice实现全局异常处理,强调代码复用、统... 目录前言一、为什么要使用全局异常处理?二、核心注解解析1. @RestControllerAdvice2

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java进程异常故障定位及排查过程

《Java进程异常故障定位及排查过程》:本文主要介绍Java进程异常故障定位及排查过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、故障发现与初步判断1. 监控系统告警2. 日志初步分析二、核心排查工具与步骤1. 进程状态检查2. CPU 飙升问题3. 内存

SpringBoot整合liteflow的详细过程

《SpringBoot整合liteflow的详细过程》:本文主要介绍SpringBoot整合liteflow的详细过程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋...  liteflow 是什么? 能做什么?总之一句话:能帮你规范写代码逻辑 ,编排并解耦业务逻辑,代码

Java中调用数据库存储过程的示例代码

《Java中调用数据库存储过程的示例代码》本文介绍Java通过JDBC调用数据库存储过程的方法,涵盖参数类型、执行步骤及数据库差异,需注意异常处理与资源管理,以优化性能并实现复杂业务逻辑,感兴趣的朋友... 目录一、存储过程概述二、Java调用存储过程的基本javascript步骤三、Java调用存储过程示

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

MySQL中的InnoDB单表访问过程

《MySQL中的InnoDB单表访问过程》:本文主要介绍MySQL中的InnoDB单表访问过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、环境3、访问类型【1】const【2】ref【3】ref_or_null【4】range【5】index【6】

MySQL中的表连接原理分析

《MySQL中的表连接原理分析》:本文主要介绍MySQL中的表连接原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、环境3、表连接原理【1】驱动表和被驱动表【2】内连接【3】外连接【4编程】嵌套循环连接【5】join buffer4、总结1、背景

在Spring Boot中集成RabbitMQ的实战记录

《在SpringBoot中集成RabbitMQ的实战记录》本文介绍SpringBoot集成RabbitMQ的步骤,涵盖配置连接、消息发送与接收,并对比两种定义Exchange与队列的方式:手动声明(... 目录前言准备工作1. 安装 RabbitMQ2. 消息发送者(Producer)配置1. 创建 Spr

浏览器插件cursor实现自动注册、续杯的详细过程

《浏览器插件cursor实现自动注册、续杯的详细过程》Cursor简易注册助手脚本通过自动化邮箱填写和验证码获取流程,大大简化了Cursor的注册过程,它不仅提高了注册效率,还通过友好的用户界面和详细... 目录前言功能概述使用方法安装脚本使用流程邮箱输入页面验证码页面实战演示技术实现核心功能实现1. 随机