grub-install源码分析---2

grub-install源码分析—2

上一章分析了grub-install源码的第一部分,该部分的主要功能是处理命令行参数,并初始化一些文件和变量,紧接下来的一部分代码用于处理即将安装的存储设备,下面来看。

grub2-install第四部分
util/grub-install.c

    ...

    size_t ndev = 0;
    grub_devices = grub_guess_root_devices (grubdir);

    for (curdev = grub_devices; *curdev; curdev++){
        grub_util_pull_device (*curdev);
        ndev++;
    }

    grub_drives = xmalloc (sizeof (grub_drives[0]) * (ndev + 1)); 

    for (curdev = grub_devices, curdrive = grub_drives; *curdev; curdev++,
            curdrive++){
        *curdrive = grub_util_get_grub_dev (*curdev);
    }
    *curdrive = 0;

    grub_dev = grub_device_open (grub_drives[0]);

    ...

变量grubdir表示grub文件所在目录,例如”/boot/grub”,首先通过grub_guess_root_devices函数找到该目录所在的根设备文件名,例如”/dev/sda3”,然后调用grub_util_pull_device函数读取设备以及设备中的分区信息,接着通过grub_util_get_grub_dev函数获取设备文件对应的驱动名,例如第一块硬盘对应的设备文件为”/dev/sda”,则其驱动名为hd0,最后调用grub_device_open函数打开该设备,返回的grub_dev结构中保存了对应的设备信息,例如硬盘的磁道数,扇区数等等。grub_device_open函数内部主要通过grub_disk_open函数打开指定设备,grub_disk_open函数在后面会分析。

grub2-install->grub_guess_root_devices
grub-core/osdep/unix/getroot.c

char ** grub_guess_root_devices (const char *dir_in){
    char **os_dev = NULL;
    struct stat st;
    dev_t dev;
    char *dir = grub_canonicalize_file_name (dir_in);

    if (!os_dev)
        os_dev = grub_find_root_devices_from_mountinfo (dir, NULL);

    if (!os_dev)
      os_dev = find_root_devices_from_libzfs (dir);

    ...

    stat (dir, &st);
    dev = st.st_dev;
    os_dev = xmalloc (2 * sizeof (os_dev[0]));
    os_dev[0] = grub_find_device ("/dev", dev);

    if (!os_dev[0]){
        free (os_dev);
        return 0;
    }

    os_dev[1] = 0;
    return os_dev;
}

传入的参数dir_in为grub目录,默认为”/boot/grub”。grub_canonicalize_file_name函数用于规范目录名,例如解析链接文件,删除一些不必要的文件分隔符,等等。

通过文件名获取根设备信息的方式有很多,接下来一一尝试。首先通过grub_find_root_devices_from_mountinfo函数尝试在linux系统下的”/proc/self/mountinfo”文件中获得根设备信息,如果获取失败,再尝试通过find_root_devices_from_libzfs函数从zfs文件系统中获取根设备信息。假设前两者都没找到,就通过stat函数获取”/boot/grub”文件信息,返回的stat结构体中的st_dev成员变量包含了根设备的主次设备号,根据该主次设备号,通过grub_find_device函数遍历”/dev”目录,查找对应的设备文件。例如stat函数返回0x0803,表示sda的第3个分区,grub_find_device函数最终返回”/dev/sda3”。

grub2-install->grub_guess_root_devices->grub_find_device
grub-core/osdep/unix/getroot.c

char * grub_find_device (const char *dir, dev_t dev){
    DIR *dp;
    char *saved_cwd;
    struct dirent *ent;

    dp = opendir (dir);
    saved_cwd = xgetcwd ();
    chdir (dir);

    while ((ent = readdir (dp)) != 0){
        struct stat st;

        if (ent->d_name[0] == '.')
            continue;

        if (lstat (ent->d_name, &st) < 0)
            continue;

        if (S_ISLNK (st.st_mode)) {
            if (strcmp (dir, "mapper") == 0 || strcmp (dir, "/dev/mapper") == 0) {
                if (stat (ent->d_name, &st) < 0)
                    continue;
            } else
                continue;
        }

        if (S_ISDIR (st.st_mode)){
            char *res;
            res = grub_find_device (ent->d_name, dev);

            if (res){
                free (saved_cwd);
                closedir (dp);
                return res;
            }
        }

        if (S_ISBLK (st.st_mode) && st.st_rdev == dev){
            if (ent->d_name[0] == 'd' &&
                ent->d_name[1] == 'm' &&
                ent->d_name[2] == '-' &&
                ent->d_name[3] >= '0' &&
                ent->d_name[3] <= '9')
                continue;

            char *res;
            char *cwd;

            cwd = xgetcwd ();
            res = xmalloc (strlen (cwd) + strlen (ent->d_name) + 3);
            sprintf (res, "%s/%s",cwd, ent->d_name);
            strip_extra_slashes (res);
            free (cwd);

            if (strcmp(res, "/dev/root") == 0){
                free (res);
                continue;
            }

            chdir (saved_cwd);
            free (saved_cwd);
            closedir (dp);
            return res;
        }
    }

    chdir (saved_cwd);
    free (saved_cwd);
    closedir (dp);
    return 0;
}

传入的参数dir为”/dev”,因此接下来就会在该目录下根据另一个参数,也即主次设备号dev查找对应的设备文件。

首先通过opendir函数打开”/dev”文件,xgetcwd函数返回当前工作目录的绝对路径,保存到saved_cwd中,然后通过chdir函数将当前工作目录切换到”/dev”中。

接下来通过while循环遍历”/dev”文件夹下的每个文件,readdir文件读取目录,返回dirent结构体,其中的d_name成员变量表示文件名,这里忽略文件名以点开始的文件(即当前目录”.”或上级目录”..”),lstat函数和前面的stat函数类似,用于获取文件信息。

如果通过S_ISLNK宏判断该文件是一个链接,此处mapper文件和linux的device mapper机制相关,此时需要重新读取文件信息,本章不关心,继续读取下一个文件。如果通过S_ISDIR判断该文件是一个目录,则递归调用grub_find_device函数在子目录下查找对应的根设备。如果通过S_ISBLK判断该文件是一个块设备,此时就要和传入的参数对比主次设备号了,如果相等,但是块设备对应的文件名为dm-数字,则该设备是lvm中的逻辑卷编号,此时忽略;否则,拷贝找到的设备文件名到res中,并返回。最后”/dev/root”表示根文件系统设备,此时也无效。

grub2-install->grub_util_pull_device
util/getroot.c

void grub_util_pull_device (const char *os_dev){
    enum grub_dev_abstraction_types ab;
    ab = grub_util_get_dev_abstraction (os_dev);
    switch (ab){
    case GRUB_DEV_ABSTRACTION_LVM:
        grub_util_pull_lvm_by_command (os_dev);
    case GRUB_DEV_ABSTRACTION_LUKS:
        grub_util_pull_devmapper (os_dev);
        return;

    default:
        if (grub_util_pull_device_os (os_dev, ab))
            return;

    case GRUB_DEV_ABSTRACTION_NONE:
        free (grub_util_biosdisk_get_grub_dev (os_dev));
        return;
    }
}

假设grub_util_get_dev_abstraction函数返回GRUB_DEV_ABSTRACTION_NONE,因此接下来执行grub_util_pull_device_os,默认情况下该函数返回0。再往下执行grub_util_pull_device_os函数,该函数默认返回0,最后执行grub_util_biosdisk_get_grub_dev函数,该函数用于查找设备文件os_dev对应的硬盘设备和分区信息,返回设备名,例如fd0、fd1等等。该函数此时只是起到了检测作用。

下面看几个重要的函数。

grub2-install->grub_util_pull_device->grub_util_get_dev_abstraction
util/getroot.c

int grub_util_get_dev_abstraction (const char *os_dev){
    enum grub_dev_abstraction_types ret;

    if (grub_util_biosdisk_is_present (os_dev))
        return GRUB_DEV_ABSTRACTION_NONE;

    ...
}

int grub_util_biosdisk_is_present (const char *os_dev){
    int ret = (find_system_device (os_dev) != NULL);
    return ret;
}

grub_util_biosdisk_is_present函数用于判断设备文件对应的设备是否能找到,假设能,最终返回GRUB_DEV_ABSTRACTION_NONE,表示普通的硬盘设备。

grub2-install->grub_util_pull_device->grub_util_get_dev_abstraction->grub_util_biosdisk_is_present->find_system_device
util/getroot.c

static const char * find_system_device (const char *os_dev){
    char *os_disk;
    const char *drive;
    int is_part;

    os_disk = convert_system_partition_to_system_disk (os_dev, &is_part);
    if (! os_disk)
        return NULL;

    drive = grub_hostdisk_os_dev_to_grub_drive (os_disk, 0);
    free (os_disk);
    return drive;
}

首先通过convert_system_partition_to_system_disk函数根据包含了分区号的设备文件名获取具体的硬盘文件路径。然后通过grub_hostdisk_os_dev_to_grub_drive函数根据硬盘文件路径获得具体的设备名,下面依次来看。

grub2-install->grub_util_pull_device->grub_util_get_dev_abstraction->grub_util_biosdisk_is_present->find_system_device->convert_system_partition_to_system_disk->grub_util_part_to_disk
grub-core/osdep/linux/getroot.c

static char * convert_system_partition_to_system_disk (const char *os_dev, int *is_part){
    struct stat st;

    stat (os_dev, &st);
    *is_part = 0;
    return grub_util_part_to_disk (os_dev, &st, is_part);
}

char * grub_util_part_to_disk (const char *os_dev, struct stat *st, int *is_part){
    char *path;

    ...

    path = xmalloc (PATH_MAX);

    if (! realpath (os_dev, path))
        return NULL;

    if (strncmp ("/dev/", path, 5) == 0){
        char *p = path + 5;

        ...

        if ((strncmp ("hd", p, 2) == 0
            || strncmp ("vd", p, 2) == 0
            || strncmp ("sd", p, 2) == 0)
            && p[2] >= 'a' && p[2] <= 'z'){
            char *pp = p + 2;
            while (*pp >= 'a' && *pp <= 'z')
                pp++;
            if (*pp)
                *is_part = 1;
            *pp = '\0';
            return path;
        }

        ...

    }

    return path;
}

convert_system_partition_to_system_disk函数进而调用grub_util_part_to_disk函数,首先通过realpath函数将文件路径os_dev转化为绝对路径path,然后开始分析该路径。首先检查路径是否以”/dev/”开头,然后检查接下来的字符是否已hd、vd和sd开头,再跳过之后的字母,最后将第一个非字母位置上的字符设置为空字符”\0”,因此最后返回未带分区信息的硬盘文件名。例如”/dev/sda1”经过该函数就变为”/dev/sda”。

grub2-install->grub_util_pull_device->grub_util_get_dev_abstraction->grub_util_biosdisk_is_present->find_system_device->grub_hostdisk_os_dev_to_grub_drive
grub-core/kern/emu/hostdisk.c

const char * grub_hostdisk_os_dev_to_grub_drive (const char *os_disk, int add){
    unsigned int i;
    char *canon;

    canon = grub_canonicalize_file_name (os_disk);
    if (!canon)
        canon = xstrdup (os_disk);

    for (i = 0; i < ARRAY_SIZE (map); i++)
        if (! map[i].device)
            break;
        else if (strcmp (map[i].device, canon) == 0){
            free (canon);
            return map[i].drive;
        }

    if (!add){
        free (canon);
        return NULL;
    }

    ...
}

全局的map数组在上一章分析的grub_util_biosdisk_init函数中初始化,该函数会读取device.map文件,将该文件的device和drive的对应关系存储到该数组中。grub_hostdisk_os_dev_to_grub_drive函数就是在该map数组中查找device,并返回对应的drive值。例如device为”/dev/sda”,其对应的drive值为”hd0”。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev
util/getroot.c

char * grub_util_biosdisk_get_grub_dev (const char *os_dev){
    const char *drive;
    char *sys_disk;
    int is_part;

    sys_disk = convert_system_partition_to_system_disk (os_dev, &is_part);
    drive = grub_hostdisk_os_dev_to_grub_drive (sys_disk, 1);

    char *name;
    grub_disk_t disk;
    struct grub_util_biosdisk_get_grub_dev_ctx ctx;

    name = make_device_name (drive);
    ctx.start = grub_util_find_partition_start (os_dev);
    disk = grub_disk_open (name);
    free (name);
    ctx.partname = NULL;

    grub_partition_iterate (disk, find_partition, &ctx);

    if (ctx.partname == NULL){
        ...
        return 0;
    }

    free (ctx.partname);
    grub_disk_close (disk);
    return name;
}

首先通过convert_system_partition_to_system_disk函数获取不带分区信息的设备文件名,前面分析过了。然后通过grub_hostdisk_os_dev_to_grub_drive函数查找设备文件对应的驱动号,前面也分析过了,这里不同之处在于传入的add参数为真,表示当在全局的map数组中找不到对应设备的驱动号时,需要在hostdisk目录下添加设备,下面假设找到了。

make_device_name主要用于规范路径中的分隔号。接下来通过grub_util_find_partition_start函数,进而通过grub_util_find_partition_start_os函数查找grub目录对应分区的起始偏移扇区数。然后通过grub_disk_open打开设备,返回的参数disk保存了硬盘的基本信息。最后调用grub_partition_iterate函数在硬盘中查找该分区,返回分区名。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_util_find_partition_start_os
grub-core/osdep/linux/hostdisk.c

grub_disk_addr_t grub_util_find_partition_start_os (const char *dev){
    grub_disk_addr_t start = 0;
    grub_util_fd_t fd;
    struct hd_geometry hdg;

    if (sysfs_partition_start (dev, &start))
        return start;

    fd = open (dev, O_RDONLY);
    ioctl (fd, HDIO_GETGEO, &hdg);

    close (fd);
    return hdg.start;
}

首先通过sysfs_partition_start函数在sysfs文件系统查找设备信息,sysfs文件系统在较早的linux内核中不存在,sysfs_partition_start函数简而言之就是在该文件系统下找到对应的设备目录,在该设备目录下读取start属性。
如果sysfs_partition_start函数获取成功则直接返回结果,否则通过open函数打开设备文件dev,再通过HDIO_GETGEO指令获取块设备的信息,其中包括heads磁头数,sectors每磁道扇区数,cylinders柱面数,以及start表示该分区的起始扇区。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_disk_open
util/getroot.c

grub_disk_t grub_disk_open (const char *name){
    const char *p;
    grub_disk_t disk;
    grub_disk_dev_t dev;
    char *raw = (char *) name;
    grub_uint64_t current_time;

    disk = (grub_disk_t) grub_zalloc (sizeof (*disk));
    disk->log_sector_size = GRUB_DISK_SECTOR_BITS;
    disk->max_agglomerate = 1048576 >> (GRUB_DISK_SECTOR_BITS
                                + GRUB_DISK_CACHE_BITS);
    disk->name = grub_strdup (name);

    for (dev = grub_disk_dev_list; dev; dev = dev->next){
        if ((dev->open) (raw, disk) == GRUB_ERR_NONE)
            break;
        else if (grub_errno == GRUB_ERR_UNKNOWN_DEVICE)
            grub_errno = GRUB_ERR_NONE;
        else
            goto fail;
    }

    disk->dev = dev;
    return disk;
}

GRUB_DISK_SECTOR_BITS宏是表示扇区大小需要的比特数,一个扇区的默认大小为512字节,因此需要9比特。GRUB_DISK_CACHE_BITS宏是表示缓存大小需要的比特数,缓存的最小单元为一个扇区,默认的最大值为最大扇区占用的比特数,例如最大扇区为16kB,因此其为16kB/512B,需要6个比特表示。两者相加就是每块缓存占用的字节数。因此max_agglomerate就是计算缓存个数的最大值。传入的参数name为设备对应的驱动号,例如fd0、fd1、hd0、hd1,等等。根据前面的分析,正常情况下,这里只能是fd0或hd0的其中一种。

接下来遍历grub_disk_dev_list列表,该列表中保存了不同设备类型的打开函数,调用open打开该设备,这里假设最终通过grub_disk_dev_list列表中的grub_biosdisk_dev结构打开该设备,该结构对应的open函数为grub_biosdisk_open,最后的信息保存在disk中并返回。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_disk_open->grub_biosdisk_open
grub-core/disk/i386/pc/biosdisk.c

static grub_err_t grub_biosdisk_open (const char *name, grub_disk_t disk){
    grub_uint64_t total_sectors = 0;
    int drive;
    struct grub_biosdisk_data *data;

    drive = grub_biosdisk_get_drive (name);
    disk->id = drive;
    data = (struct grub_biosdisk_data *) grub_zalloc (sizeof (*data));
    data->drive = drive;

    if ((cd_drive) && (drive == cd_drive)){
        ...
    }else{
        int version;
        disk->log_sector_size = 9;

        version = grub_biosdisk_check_int13_extensions (drive);
        if (version){
            struct grub_biosdisk_drp *drp
                = (struct grub_biosdisk_drp *) GRUB_MEMORY_MACHINE_SCRATCH_ADDR;

            grub_memset (drp, 0, sizeof (*drp));
            drp->size = sizeof (*drp);
            if (! grub_biosdisk_get_diskinfo_int13_extensions (drive, drp)){
                data->flags = GRUB_BIOSDISK_FLAG_LBA;

                if (drp->total_sectors)
                    total_sectors = drp->total_sectors;
                else
                    total_sectors = ((grub_uint64_t) drp->cylinders) * drp->heads * drp->sectors;

                if (drp->bytes_per_sector
                    && !(drp->bytes_per_sector & (drp->bytes_per_sector - 1))
                    && drp->bytes_per_sector >= 512
                    && drp->bytes_per_sector <= 16384){
                    for (disk->log_sector_size = 0;
                        (1 << disk->log_sector_size) < drp->bytes_per_sector;
                        disk->log_sector_size++);
                }
            }
        }
    }

    if (! (data->flags & GRUB_BIOSDISK_FLAG_CDROM)){
        if (grub_biosdisk_get_diskinfo_standard (drive,
                           &data->cylinders,
                           &data->heads,
                           &data->sectors) != 0){
            ...            
        }
    }

    disk->total_sectors = total_sectors;
    disk->max_agglomerate = 0x7f >> GRUB_DISK_CACHE_BITS;
    disk->data = data;

    return GRUB_ERR_NONE;
}

假设传入的参数name为hd0,首先通过grub_biosdisk_get_drive函数获取硬盘的最终驱动号drive。然后调用grub_biosdisk_check_int13_extensions利用BIOS的int 13中断获得硬盘LBA模式的主版本号version。如果version不为空,就通过参数为0x42的int 0x13中断获取硬盘信息。GRUB_MEMORY_MACHINE_SCRATCH_ADDR为从硬盘中读取的信息的存放地址,默认为0x68000,在该地址上的结构体grub_biosdisk_drp用于保存硬盘的基本信息。

接下来调用grub_biosdisk_get_diskinfo_int13_extensions函数,进而调用参数为0x48的int 0x13中断,读取硬盘信息,将结果保存在drp结构中。如果读取成功返回0,设置flags为GRUB_BIOSDISK_FLAG_LBA,表示LBA模式。
读取的信息中,total_sectors表示硬盘的总扇区数,如果中断没有返回,就自行计算。正常情况下扇区的字节数bytes_per_sector为512字节,转化为比特数log_sector_size默认为9,如果bytes_per_sector不为512字节,就要对该字节数占用的比特数log_sector_size进行相应的调整。

获取完硬盘的总扇区数后,接下来通过grub_biosdisk_get_diskinfo_standard函数获取磁盘的cylinders、heads和sectors参数,其中heads表示有几个磁头,即几个盘面,cylinders表示每个盘面的磁道数,sectors表示每个磁道的扇区数。该函数内部通过参数为0x8的int 13中断读取磁盘参数,和前面的grub_biosdisk_check_int13_extensions函数类似,这里就不往下分析了。这里注意一下,传统的硬盘每磁道的扇区数都相同,例如都为64,这样外圈的磁道的存储密度会低于内圈的存储密度,使用LBA相对寻址技术后,能保证扇区在不论外圈还是内圈都均匀分布,因此外圈的扇区数会明显大于内存的扇区数,此时通过grub_biosdisk_get_diskinfo_standard函数读取出来的sectors参数是一个每磁道扇区数的平均值。

最后将这些结果保存在disk中并返回。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_disk_open->grub_biosdisk_open->grub_biosdisk_get_drive
grub-core/disk/i386/pc/biosdisk.c

static int grub_biosdisk_get_drive (const char *name) {
    unsigned long drive;

    if ((name[0] != 'f' && name[0] != 'h') || name[1] != 'd')
        goto fail;

    drive = grub_strtoul (name + 2, 0, 10);
    if (name[0] == 'h')
        drive += 0x80;

    return (int) drive ;
}

假设传入的参数name为hd0。首先通过grub_strtoul函数获得hd后面跟着的数字0。如果是以hd开头的硬盘,则需要将驱动号加上0x80,即hd0最终对应的驱动号为0x80,类推hd1,最终的驱动号为0x81。从该函数也可以看出,SCSI和IDE类型的硬盘都是从0x80开始计数,系统并没有区分该类型。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_disk_open->grub_biosdisk_open->grub_biosdisk_get_drive->grub_strtoull
grub-core/kern/misc.c

unsigned long long grub_strtoull (const char *str, char **end, int base){
    unsigned long long num = 0;
    int found = 0;

    while (grub_isspace (*str))
        str++;

    if (str[0] == '0'){
        if (str[1] == 'x'){
            if (base == 0 || base == 16){
                base = 16;
                str += 2;
            }
        } else if (base == 0 && str[1] >= '0' && str[1] <= '7')
            base = 8;
    }

    if (base == 0)
        base = 10;

    while (*str){
        unsigned long digit;

        digit = grub_tolower (*str) - '0';
        if (digit > 9){
            digit += '0' - 'a' + 10;
            if (digit >= (unsigned long) base)
                break;
        }
        found = 1;

        num = num * base + digit;
        str++;
    }

    if (end)
        *end = (char *) str;

    return num;
}

首先略过字符串中的起始空格。第一个if判断hd后面跟着的是八进制数或者十六进制数,并设置base为8或者16,
默认base为10,表示10进制。grub_tolower通过字符获得具体的数字,如果该数字大于9,表示是16进制数,也表示后面跟着的是英文字母,需要从9开始计算。num最后统计hd后面跟着的数字是多少,最后的end设置字符串的结尾。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_disk_open->grub_biosdisk_open->grub_biosdisk_check_int13_extensions
grub-core/disk/i386/pc/biosdisk.c

static int grub_biosdisk_check_int13_extensions (int drive){
    struct grub_bios_int_registers regs;

    regs.edx = drive & 0xff;
    regs.eax = 0x4100;
    regs.ebx = 0x55aa;
    regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT;
    grub_bios_interrupt (0x13, &regs);

    if (regs.flags & GRUB_CPU_INT_FLAGS_CARRY)
       return 0;

    if ((regs.ebx & 0xffff) != 0xaa55)
        return 0;

    if (!(regs.ecx & 1))
        return 0;

    return (regs.eax >> 8) & 0xff;
}

该函数主要通过grub_bios_interrupt函数进入实模式,调用BIOS的int 0x13中断,参数ah为0x41,该中断用于检查磁盘拓展模式。硬盘有LBA和CHS两种模式,简单说CHS模式支持的硬盘容量较小,并且完全按照硬盘的硬件结构进行寻址并读写,LBA模式采用逻辑寻址,支持的硬盘容量多达TB级别,因此现在大多都使用LBA模式了。
返回结果ebx保存了魔数0xaa55,ecx保存了位图信息,表示当参数为0x42时,int 0x13中断是否可用。最终返回的ah寄存器中保存了LBA模式的主版本号。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_disk_open->grub_biosdisk_open->grub_biosdisk_get_diskinfo_int13_extensions
grub-core/disk/i386/pc/biosdisk.c

static int grub_biosdisk_get_diskinfo_int13_extensions (int drive, void *drp){
    return grub_biosdisk_get_diskinfo_real (drive, drp, 0x4800);
}

static int grub_biosdisk_get_diskinfo_real (int drive, void *drp, grub_uint16_t ax){
    struct grub_bios_int_registers regs;

    regs.eax = ax;
    regs.esi = ((grub_addr_t) drp) & 0xf;
    regs.ds = ((grub_addr_t) drp) >> 4;
    regs.edx = drive & 0xff;

    regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT;
    grub_bios_interrupt (0x13, &regs);

    if ((regs.flags & GRUB_CPU_INT_FLAGS_CARRY) && ((regs.eax & 0xff00) != 0))
        return (regs.eax & 0xff00) >> 8;

    return 0;
}

ah寄存器为0x48表示读取硬盘参数,ds:esi是接收数据存放的实模式下的地址,32位edx寄存器的低8位,也即dx寄存器表示硬盘号,也即前面的drive,例如0x80,最后返回的ah寄存器存放了返回码。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_partition_iterate
grub-core/kern/partition.c

int grub_partition_iterate (struct grub_disk *disk,
            grub_partition_iterate_hook_t hook, void *hook_data){
    struct grub_partition_iterate_ctx ctx = {
        .ret = 0,
        .hook = hook,
        .hook_data = hook_data
    };
    const struct grub_partition_map *partmap;

    FOR_PARTITION_MAPS(partmap){
        grub_err_t err;
        err = partmap->iterate (disk, part_iterate, &ctx);
        if (err)
          grub_errno = GRUB_ERR_NONE;
        if (ctx.ret)
          break;
    }

    return ctx.ret;
}

创建的ctx是上下文,用来保存返回的数据,hook_data包含了分区的参数,也用于保存最终的分区名partname。FOR_PARTITION_MAPS宏用于遍历grub_partition_map_list列表,执行相应分区类型对应的iterate函数。下面假设执行了grub_partition_msdos_iterate函数。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_partition_iterate->grub_partition_msdos_iterate
grub-core/partmap/mdsoc.c

grub_err_t grub_partition_msdos_iterate (grub_disk_t disk,
                  grub_partition_iterate_hook_t hook, void *hook_data){

    struct grub_partition p;
    struct grub_msdos_partition_mbr mbr;
    int labeln = 0;
    grub_disk_addr_t lastaddr;
    grub_disk_addr_t ext_offset;
    grub_disk_addr_t delta = 0;

    p.offset = 0;
    ext_offset = 0;
    p.number = -1;
    p.partmap = &grub_msdos_partition_map;

    lastaddr = !p.offset;

    while (1){
        int i;
        struct grub_msdos_partition_entry *e;

        if (grub_disk_read (disk, p.offset, 0, sizeof (mbr), &mbr))
            goto finish;

        if (p.offset == 0)
            for (i = 0; i < 4; i++)
                if (mbr.entries[i].type == GRUB_PC_PARTITION_TYPE_GPT_DISK)
                    return grub_error (GRUB_ERR_BAD_PART_TABLE, "dummy mbr");

        if (labeln && lastaddr == p.offset)
            return grub_error (GRUB_ERR_BAD_PART_TABLE, "loop detected");

        labeln++;
        if ((labeln & (labeln - 1)) == 0)
            lastaddr = p.offset;

        if (mbr.signature != grub_cpu_to_le16_compile_time (GRUB_PC_PARTITION_SIGNATURE))
            return grub_error (GRUB_ERR_BAD_PART_TABLE, "no signature");

        for (i = 0; i < 4; i++)
            if (mbr.entries[i].flag & 0x7f)
                return grub_error (GRUB_ERR_BAD_PART_TABLE, "bad boot flag");

        for (p.index = 0; p.index < 4; p.index++){
            e = mbr.entries + p.index;

            p.start = p.offset + (grub_le_to_cpu32 (e->start)
                << (disk->log_sector_size - GRUB_DISK_SECTOR_BITS)) - delta;
            p.len = grub_le_to_cpu32 (e->length) << (disk->log_sector_size - GRUB_DISK_SECTOR_BITS);
            p.msdostype = e->type;

            if (! grub_msdos_partition_is_empty (e->type)
                && ! grub_msdos_partition_is_extended (e->type)){
                p.number++;
                if (hook (disk, &p, hook_data))
                    return grub_errno;
            }else if (p.number < 3)
                p.number++;
        }

        for (i = 0; i < 4; i++){
            e = mbr.entries + i;

            if (grub_msdos_partition_is_extended (e->type)){
                p.offset = ext_offset + (grub_le_to_cpu32 (e->start)
                            << (disk->log_sector_size - GRUB_DISK_SECTOR_BITS));
                if (! ext_offset)
                    ext_offset = p.offset;

                break;
            }
        }

        if (i == 4)
            break;
    }

finish:
    return grub_errno;
}

循环之前设置lastaddr是为了保证第一次循环时lastaddr的初始值和offset不相等,其实就是跳过loop检查,所谓loop检查,是为了防止分区表出现循环的现象。

接下来进入while循环,开始遍历分区表,首先通过grub_disk_read函数读取对应的引导扇区,第一次读取时offset为0,因此读取的是主引导扇区,之后的读取主要是在拓展分区中读取分区表形成的链表结构,此时读取的是拓展分区中对应逻辑分区的引导扇区。读取引导扇区mbr后,如果是主引导扇区,需要遍历分区表entries,如果分区表中的4个分区有某个分区的类型为GPT,则报错(GPT分区方式有对应的iterate函数处理)。接下来的lastaddr和labeln的计算用于检查loop,注意这里labeln的计算是为了后面比较本次读取的偏移地址,和前一个2的幂次方位置上读取时的偏移地址,举个例子,假设本次是第7次读取,则需要和第4次读取的偏移地址作比较,如果两者相等,即lastaddr和offset相等,则报错。然后比较签名mbr.signature,默认为0xaa55。flags指示该分区是否有效,默认为0x00,当该分区为可引导分区时,值为0x80。

再往下遍历刚刚读取到的分区表entries,分别计算分区的起始扇区偏移地址start,分区的总扇区数len和分区的类型msdostype。然后判断该分区类型,如果既不是一个无效分区,也不是一个拓展分区,则递增number的值,并调用hook函数,也即part_iterate函数在该分区中查找。

然后,再一次遍历分区表entries,判断如果该分区是一个拓展分区,则重新设置offset的值。其中,ext_offset初始值为0,第一次读取拓展分区后,便记录了拓展分区的起始地址,在后续的读取中,将其加上某个逻辑分区的相对起始地址start,便得到逻辑分区的绝对地址。

最后,如果i等于4,表示读取完了某个引导分区的所有分区表项,此时退出循环。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_partition_iterate->grub_partition_msdos_iterate->grub_disk_read
grub-core/kern/disk.c

grub_err_t grub_disk_read (grub_disk_t disk, grub_disk_addr_t sector,
        grub_off_t offset, grub_size_t size, void *buf){

    if (grub_disk_adjust_range (disk, &sector, &offset, size) != GRUB_ERR_NONE){
        ...
    }

    while (size >= (GRUB_DISK_CACHE_SIZE << GRUB_DISK_SECTOR_BITS)){
        char *data = NULL;
        grub_disk_addr_t agglomerate;
        grub_err_t err;

        for (agglomerate = 0; agglomerate
            < (size >> (GRUB_DISK_SECTOR_BITS + GRUB_DISK_CACHE_BITS))
            && agglomerate < disk->max_agglomerate;agglomerate++){
            data = grub_disk_cache_fetch (disk->dev->id, disk->id,
                    sector + (agglomerate << GRUB_DISK_CACHE_BITS));
            if (data)
                break;
        }

        if (data){
            grub_memcpy ((char *) buf + (agglomerate << (GRUB_DISK_CACHE_BITS + 
                GRUB_DISK_SECTOR_BITS)), data, GRUB_DISK_CACHE_SIZE << GRUB_DISK_SECTOR_BITS);
        }

        if (agglomerate){
            grub_disk_addr_t i;
            (disk->dev->read) (disk, transform_sector (disk, sector),
                agglomerate << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS
                 - disk->log_sector_size),buf);

            for (i = 0; i < agglomerate; i ++)
                grub_disk_cache_store (disk->dev->id, disk->id, 
                    sector + (i << GRUB_DISK_CACHE_BITS),
                    (char *) buf + (i << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS)));

            sector += agglomerate << GRUB_DISK_CACHE_BITS;
            size -= agglomerate << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS);
            buf = (char *) buf + (agglomerate << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS));
        }

        if (data){
            sector += GRUB_DISK_CACHE_SIZE;
            buf = (char *) buf + (GRUB_DISK_CACHE_SIZE << GRUB_DISK_SECTOR_BITS);
            size -= (GRUB_DISK_CACHE_SIZE << GRUB_DISK_SECTOR_BITS);
        }
    }

    if (size){
        grub_disk_read_small (disk, sector, 0, size, buf);
    }

    return grub_errno;
}

传入的参数disk中保存了硬盘信息,sector表示从第几个扇区开始读,offset表示字节的偏移数,size表示需要读取的扇区数,buf指针用于保存返回结果。
首先通过grub_disk_adjust_range函数检查即将读取的扇区是否在drive指定的扇区范围内。

接下来通过循环读取数据,GRUB_DISK_CACHE_SIZE宏定义了一次读取最大的扇区数,默认为6,表示64个扇区,换算成字节数为16KB,为了方便说明,这里讲16KB的数据称为“块”。因此当size大于1块时,一次最大只能读取1块数据,agglomerate参数就表示是第几块,max_agglomerate参数则规定了最多读取的扇区数,由前面的定义可知,最多只能读取0x7f个扇区数据。

每次循环中首先通过grub_disk_cache_fetch函数获取缓存中的数据,传入的参数id假设为grub_biosdisk_dev中定义的GRUB_DISK_DEVICE_BIOSDISK_ID,disk->id是在grub_biosdisk_get_drive函数中计算的驱动号,例如第一个块硬盘为0x80。如果缓存中的值存在,则退出循环,并通过grub_memcpy函数将数据拷贝到buf中,注意这里是按块拷贝。

接下来如果agglomerate不为0,则说明agglomerate之前的数据在缓存中找不到。例如, 假设agglomerate为3,则表示可能在缓存中找到了3对应的块,此时0,1,2对应的块在缓存中肯定找不到。需要通过具体设备的read函数读取硬盘数据,读取的长度由agglomerate的值决定,例如agglomerate为3,就要读取3块数据。读取完成后,在buf中遍历每块数据,调用grub_disk_cache_store函数存入缓存,该函数和缓存的取函数grub_disk_cache_fetch类似,这里不往下看了,最后调整各个参数。

最后如果剩余大小不满足一个块的大小,即默认小于16KB,则调用grub_disk_read_small读取剩余数据,grub_disk_read_small和该函数类似,就不分析了。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_partition_iterate->grub_partition_msdos_iterate->grub_disk_read->grub_disk_adjust_range
grub-core/kern/disk-common.c

static grub_err_t grub_disk_adjust_range (grub_disk_t disk, grub_disk_addr_t *sector,
            grub_off_t *offset, grub_size_t size)
{
    grub_partition_t part;
    grub_disk_addr_t total_sectors;

    *sector += *offset >> GRUB_DISK_SECTOR_BITS;
    *offset &= GRUB_DISK_SECTOR_SIZE - 1;

    total_sectors = disk->total_sectors << (disk->log_sector_size - GRUB_DISK_SECTOR_BITS);

    if ((total_sectors <= *sector || ((*offset + size + GRUB_DISK_SECTOR_SIZE - 1)
       >> GRUB_DISK_SECTOR_BITS) > total_sectors - *sector))
        return grub_error (GRUB_ERR_OUT_OF_RANGE, N_("attempt to read or write outside of disk `%s'"), disk->name);

  return GRUB_ERR_NONE;
}

首先将字节的偏移数offset右移得出offset对应的扇区数,加上sector表示最终读取的起始扇区位置。然后将offset作与运算,得到offset在扇区内的偏移。接下来如果有些硬盘的扇区大小不是512字节,则对总扇区数total_sectors进行相应的调整。 如果对应硬盘的总扇区数total_sectors小于读取的起始扇区sector,或者读取的结束位置大于total_sectors,则报错,否则返回GRUB_ERR_NONE。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_partition_iterate->grub_partition_msdos_iterate->grub_disk_read->grub_disk_cache_fetch
grub-core/kern/disk.c

static char * grub_disk_cache_fetch (unsigned long dev_id, unsigned long disk_id,
               grub_disk_addr_t sector)
{
    struct grub_disk_cache *cache;
    unsigned cache_index;

    cache_index = grub_disk_cache_get_index (dev_id, disk_id, sector);
    cache = grub_disk_cache_table + cache_index;

    if (cache->dev_id == dev_id && cache->disk_id == disk_id && cache->sector == sector){
        cache->lock = 1;
        return cache->data;
    }

    return 0;
}

传入的参数dev_id是grub_biosdisk_dev中定义的GRUB_DISK_DEVICE_BIOSDISK_ID,代表设备类型,参数disk_id是通过grub_biosdisk_get_drive函数计算的硬盘最终驱动号,例如0x80。首先通过grub_disk_cache_get_index函数根据传入的参数计算缓存数组grub_disk_cache_table的索引cache_index,利用该索引获得对应位置的缓存cache,再比较之后返回对应的值data。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_partition_iterate->grub_partition_msdos_iterate->grub_disk_read->grub_biosdisk_read
grub-core/disk/i386/pc/biosdisk.c

static grub_err_t grub_biosdisk_read (grub_disk_t disk, grub_disk_addr_t sector,
            grub_size_t size, char *buf){
    while (size){
        grub_size_t len;
        len = get_safe_sectors (disk, sector);

        if (len > size)
            len = size;

        if (grub_biosdisk_rw (GRUB_BIOSDISK_READ, disk, sector, len,
                GRUB_MEMORY_MACHINE_SCRATCH_SEG))
            return grub_errno;

        grub_memcpy (buf, (void *) GRUB_MEMORY_MACHINE_SCRATCH_ADDR, len << disk->log_sector_size);

        buf += len << disk->log_sector_size;
        sector += len;
        size -= len;
    }

    return grub_errno;
}

grub_biosdisk_read是硬盘设备具体的读取函数,传入的参数sector是读取的起始扇区数,size是读取的总扇区数,buf是读取数据的缓存地址。
进入循环后,首先通过get_safe_sectors函数获取单次读取的扇区数len,因为一次读取是按磁道读取,假设每个磁道包含扇区的平均值为64,当读取的偏移扇区数sector为0时,本次读取的扇区数len就为64-(0%64)为64个扇区,又例如,当读取的偏移扇区数sector为127时,本次读取的扇区数len就为64-(127%64)为1个扇区,这样保证每次读取的扇区数最大就为每磁道扇区数的平均值。如果本次读取的扇区数len大于需要读取的剩余扇区数size,就调整len参数。

grub_biosdisk_rw函数执行具体的读硬盘操作,因为是通过int 13中断在实模式下读取硬盘,为了防止buf缓存地址大于20位,这里需要先将读取到的数据保存到宏GRUB_MEMORY_MACHINE_SCRATCH_ADDR定义的地址处,宏GRUB_MEMORY_MACHINE_SCRATCH_SEG是该地址对应的段地址。
读取完成后,通过grub_memcpy函数将读取的数据从GRUB_MEMORY_MACHINE_SCRATCH_ADDR地址处拷贝到缓存buf中。

最后调整各个参数,循环进行下一次读取。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_partition_iterate->grub_partition_msdos_iterate->grub_disk_read->grub_biosdisk_read->grub_biosdisk_rw
grub-core/disk/i386/pc/biosdisk.c

static grub_err_t grub_biosdisk_rw (int cmd, grub_disk_t disk,
          grub_disk_addr_t sector, grub_size_t size, unsigned segment){

    struct grub_biosdisk_data *data = disk->data;

    if (data->flags & GRUB_BIOSDISK_FLAG_LBA){
        struct grub_biosdisk_dap *dap;

        dap = (struct grub_biosdisk_dap *) (GRUB_MEMORY_MACHINE_SCRATCH_ADDR
                      + (data->sectors << disk->log_sector_size));
        dap->length = sizeof (*dap);
        dap->reserved = 0;
        dap->blocks = size;
        dap->buffer = segment << 16;
        dap->block = sector;
        if (data->flags & GRUB_BIOSDISK_FLAG_CDROM){
            ...
        }else if (grub_biosdisk_rw_int13_extensions (cmd + 0x42, data->drive, dap)){
            data->flags &= ~GRUB_BIOSDISK_FLAG_LBA;
            disk->total_sectors = data->cylinders * data->heads * data->sectors;
            return grub_biosdisk_rw (cmd, disk, sector, size, segment);
        }
    }else{
        ...
    }

    return GRUB_ERR_NONE;
}

grub_biosdisk_rw函数执行硬盘具体的读操作,下面只以LBA硬盘为例。首先将请求参数存入一个grub_biosdisk_dap结构中,注意在存储buffer的时候,将segment参数左移16位是将实模式下的段地址存储在buffer的高16位。

接下来通过grub_biosdisk_rw_int13_extensions函数,利用参数为0x42的int 13中断读取数据,如果成功则返回,如果失败,就利用传统的CHS模式再读取一次。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_partition_iterate->grub_partition_msdos_iterate->part_iterate
grub-core/kern/partition.c

static int part_iterate (grub_disk_t dsk, const grub_partition_t partition, void *data){
    struct grub_partition_iterate_ctx *ctx = data;
    struct grub_partition p = *partition;

    p.parent = dsk->partition;
    dsk->partition = 0;
    if (ctx->hook (dsk, &p, ctx->hook_data)){
        ctx->ret = 1;
        return 1;
    }

    ...

    dsk->partition = p.parent;
    return ctx->ret;
}

part_iterate函数是传入前面grub_partition_msdos_iterate函数的钩子函数,ctx参数宏的hook函数为grub_util_biosdisk_get_grub_dev函数中设置的find_partition,最后如果找到了则返回1。省略的部分和某些分区类型中的循环遍历有关,本章不考虑。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_partition_iterate->grub_partition_msdos_iterate->part_iterate->find_partition
util/getroot.c

static int find_partition (grub_disk_t dsk __attribute__ ((unused)),
        const grub_partition_t partition, void *data){

    struct grub_util_biosdisk_get_grub_dev_ctx *ctx = data;
    grub_disk_addr_t part_start = 0;

    part_start = grub_partition_get_start (partition);

    if (ctx->start == part_start){
        ctx->partname = grub_partition_get_name (partition);
        return 1;
    }

    return 0;
}

首先通过grub_partition_get_start获得该分区的绝对起始扇区数,因为参数partition中的起始扇区数start已经在grub_partition_msdos_iterate函数中计算为绝对起始扇区数了,因此该函数这里什么也没做。接下来比较该分区的起始扇区数part_start和需要比较的起始扇区数start,需要比较的起始扇区数在grub_util_find_partition_start_os函数中获得,如果相等,就调用grub_partition_get_name函数获取分区名,并返回1。

grub2-install->grub_util_pull_device->grub_util_biosdisk_get_grub_dev->grub_partition_iterate->grub_partition_msdos_iterate->part_iterate->find_partition->grub_partition_get_name
grub-core/kern/parition.c

char * grub_partition_get_name (const grub_partition_t partition){
    char *out = 0, *ptr;
    grub_size_t needlen = 0;
    grub_partition_t part;

    needlen += grub_strlen (part->partmap->name) + 1 + 27;
    out = grub_malloc (needlen + 1);

    ptr = out + needlen;
    *ptr = 0;

    char buf[27];
    grub_size_t len;
    grub_snprintf (buf, sizeof (buf), "%d", part->number + 1);
    len = grub_strlen (buf);
    ptr -= len;
    grub_memcpy (ptr, buf, len);
    len = grub_strlen (part->partmap->name);
    ptr -= len;
    grub_memcpy (ptr, part->partmap->name, len);
    *--ptr = ',';

    grub_memmove (out, ptr + 1, out + needlen - ptr);
    return out;
}

首先分配长度为needlen的内存空间out,ptr指针指向该段内存的末尾,然后先向ptr指针处写入第几个分区,例如是第一个分区number,则写入1,再向其中写入partmap的name变量,这里假设partmap为grub_msdos_partition_map,则其name为msdos,不同的分区通过逗号分隔,最后通过grub_memmove函数将ptr起始处的内存搬到out上并返回。因此最后返回的分区名形如”msdos1,msdos2”。

grub2-install->grub_util_get_grub_dev
util/getroot.c

char * grub_util_get_grub_dev (const char *os_dev){
    char *ret;

    grub_util_pull_device (os_dev);

    ret = grub_util_get_devmapper_grub_dev (os_dev);
    if (ret)
        return ret;
    ret = grub_util_get_grub_dev_os (os_dev);
    if (ret)
        return ret;

    return grub_util_biosdisk_get_grub_dev (os_dev);
}

grub_util_get_grub_dev函数主要根据设备文件名,例如/dev/sda,获得对应额设备驱动名,例如hd0,
grub_util_pull_device主要起检测作用,前面分析过了,最后通过grub_util_biosdisk_get_grub_dev函数计算并返回设备驱动名。该函数在前面也分析过了。

### 回答1: "grub-install /dev/sda" 失败可能是由于以下几种原因之一: 1. 硬盘驱动器不存在或无法识别。 2. 系统磁盘权限问题,您没有在终端中使用管理员权限运行命令。 3. 系统中已经存在其他引导程序,导致冲突。 4. grub程序本身存在问题。 建议您检查硬盘驱动器是否存在并使用管理员权限运行命令,检查系统中是否存在其他引导程序并尝试重装grub程序。 ### 回答2: 当使用“grub-install /dev/sda”命令时,如果出现失败的情况,可能会有多种问题: 1. 硬盘分区表错误:如果硬盘上的分区表损坏或不可读,则可能导致grub-install命令失败。此时需要修复分区表。 2. 文件系统损坏:如果安装GRUB的分区上的文件系统损坏,则 GRUB 可能无法正确地读取/写入相关文件,导致grub-install失败。此时需要手动修复文件系统。 3. MBR错误:如果硬盘上的主引导记录 (MBR) 损坏,则会阻止grub-install命令成功。此时需要还原 MBR,然后重新运行 grub-install 命令。 4. 磁盘容量问题:如果你的硬盘容量太小,可能会导致grub-install失败。此时需要扩大磁盘容量或使用其他磁盘。 5. 存储设备名称错误:可能在参数中使用了错误的设备名称。这个问题可以通过检查设备名称或使用分区UUID解决。 如果以上方法仍然不能解决问题,可能需要重新安装操作系统或与Linux发行版社区联系以获取解决方案。 ### 回答3: 出现“grub-install /dev/sda失败”这个问题是因为系统软件出现了错误,导致grub-install命令无法执行成功。通常出现这个问题的原因有很多种,如操作系统文件被损坏、磁盘空间不足、磁盘分区错误等。 针对这个问题,我们可以采取以下的方法进行解决: 1、检查系统文件是否损坏。出现该问题的原因可能是因为操作系统文件被破坏或损坏,可以使用系统修复工具或者恢复功能进行修复。 2、检查磁盘空间是否充足。如果磁盘空间不足,可清理一些不必要的文件,如缓存、日志等。 3、检查磁盘分区是否正确。磁盘分区错误也可能导致该问题的出现,可以使用磁盘工具重新分区。 4、使用其他方式安装grub。如果以上方法无法解决该问题,可以考虑使用其他方式安装grub,如使用live CD或者从网络中下载grub进行安装。 总之,“grub-install /dev/sda失败”这个问题出现的原因有很多种,解决方法也有多种。根据具体情况进行排查,选择恰当的解决方法才能解决该问题。同时,为了避免该问题出现,建议定期备份数据,保证系统软件不会出现问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值