关于chroot
chroot在man文档中有两种指代,1是指chroot命令,2是指chroot系统调用,两者指代相近,命令工具是chroot的具体实现,chroot系统调用则是glibc的对其的封装。
命令官方文档的描述为
使用指定root目录执行命令或者交互式shell
由于linux文件系统是以root目录为根,以树状姿态散开,这就决定了我们根下层,无法访问根上层的目录,这样就可以视作一种简单的沙箱机制。当然和docker相比,这种沙箱机制略有些简陋,且chroot出现的比较早,那时候也还没有各种namespace的概念。
我们从简单的使用中观察chroot的一些特征。我们以alpine镜像作为rootfs:
sudo chroot ./alpine /bin/sh
这样之后就会进入alpine,并与之sh进行交互。
echo $$
输出248594,说明进程的确没有隔离。
lsof -p 248594
查看该进程的一些信息,可看到rtd标志值即为alpine镜像所在根目录。
chroot的用户态
chroot命令工具解析
chroot的具体实现有很多,我采用的GUN coreutils中所携带的chroot方案。 核心代码:
if (chroot (newroot) != 0)
die (EXIT_CANCELED, errno, _("cannot change root directory to %s"),
quoteaf (newroot));
if (! skip_chdir && chdir ("/"))
die (EXIT_CANCELED, errno, _("cannot chdir to root directory"));
if (argc == optind + 1)
{
/* No command. Run an interactive shell. */
char *shell = getenv ("SHELL");
if (shell == NULL)
shell = bad_cast ("/bin/sh");
argv[0] = shell;
argv[1] = bad_cast ("-i");
argv[2] = NULL;
}
else
{
/* The following arguments give the command. */
argv += optind + 1;
}
......
/* Execute the given command. */
execvp (argv[0], argv);
核心实现逻辑较为简单:
- chroot(newroot)执行chroot系统调用切换根
- chdir("/")执行chdir切换到当前目录到"/",进而形成沙箱机制。
- execvp (argv[0], argv)执行指定命令或默认SHELL
chroot系统调用解析
具体实现里面,chroot(newroot)函数来自<unistd.h>,我本机由glibc实现,glibc对系统调用进行了封装。
glibc对于系统调用的封装比较“隐晦”,我追了良久,最后也是借助一些资料才理清了头绪。
- 查看chroot的汇编实现
下载glibc进行编译,make之后,从libc.a中解压出chroot.o,当然从libc.so中也能找到,就是需要首先确定chroot所在地址。不能从本机自带的中去看,本机release版本会摘去很多有用信息。
objdump -S -l chroot.o
chroot.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <chroot>:
chroot():
/home/yiran/code/c/glibc-2.36/misc/../sysdeps/unix/syscall-template.S:120
# if SYSCALL_ULONG_ARG_1
T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS,
SYSCALL_ULONG_ARG_1, SYSCALL_ULONG_ARG_2)
# else
T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
0: b8 a1 00 00 00 mov $0xa1,%eax
5: 0f 05 syscall
7: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax
d: 73 01 jae 10 <chroot+0x10>
/home/yiran/code/c/glibc-2.36/misc/../sysdeps/unix/syscall-template.S:122
# endif
ret
f: c3 ret
/home/yiran/code/c/glibc-2.36/misc/../sysdeps/unix/syscall-template.S:123
T_PSEUDO_END (SYSCALL_SYMBOL)
10: 48 8b 0d 00 00 00 00 mov 0x0(%rip),%rcx # 17 <chroot+0x17>
17: f7 d8 neg %eax
19: 64 89 01 mov %eax,%fs:(%rcx)
1c: 48 83 c8 ff or $0xffffffffffffffff,%rax
20: c3 ret
这里注意要用-S而不是-d,我之前在这里用-d,舍去了一些宏定义,就一直没想明白,syscall之前怎么没有传参数的动作,其实都在宏里面。
- 追PSEUDO
T_PSEUDO-><sysdep.h>(PSEUDO)
# if SYSCALL_ULONG_ARG_1
# define PSEUDO(name, syscall_name, args, ulong_arg_1, ulong_arg_2) \
.text; \
ENTRY (name) \
DO_CALL (syscall_name, args, ulong_arg_1, ulong_arg_2); \
cmpq $-4095, %rax; \
jae SYSCALL_ERROR_LABEL
# else
# define PSEUDO(name, syscall_name, args) \
.text; \
ENTRY (name) \
DO_CALL (syscall_name, args, 0, 0); \
cmpq $-4095, %rax; \
jae SYSCALL_ERROR_LABEL
# endif
- 追ENTRY
# define ENTRY(name) \
.align 4 ASM_LINE_SEP \
.globl C_SYMBOL_NAME(name) ASM_LINE_SEP \
.type C_SYMBOL_NAME(name),%function ASM_LINE_SEP \
C_LABEL(name) ASM_LINE_SEP \
cfi_startproc ASM_LINE_SEP \
CALL_MCOUNT
......
# define C_LABEL(name) name##
# define cfi_startproc .cfi_startproc
从.cfi_startproc关键字可以看出ENTRY是定义了一个名为name的函数来供调用。
- 追DO_CALL
# define DO_CALL(syscall_name, args, ulong_arg_1, ulong_arg_2) \
DOARGS_##args \
ZERO_EXTEND_##ulong_arg_1 \
ZERO_EXTEND_##ulong_arg_2 \
movl $SYS_ify (syscall_name), %eax; \
syscall;
看到syscall基本就能断定已经找对地方了,在syscall之前进行了传参,并传递了系统调用号。至此,用户态这边的行为到此为止,接下来是内核进行工作了。