上一章分析了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, ®s);
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, ®s);
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, §or, &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函数计算并返回设备驱动名。该函数在前面也分析过了。