Вдохновленный Лиз Райс и ее прекрасной статьей Strace в 60 строках Go, я подумал, что смогу ли я сделать следующий шаг в попытке декодировать файловую систему ioctl. ioctl — это системный вызов, специфичный для базового устройства или файловой системы. Это позволяет файловым системам добавлять специальные функции, которые в противном случае были бы недоступны через стандартные интерфейсы системных вызовов. Здесь — это окончательный код Go, описанный ниже.

Так почему это интересно? Возьмем простой пример ioctl из файловой системы XFS. XFS_IOC_FSCOUNTS ioctl возвращает некоторые подсчеты свободных данных, свободных экстентов в реальном времени, свободных инодов и выделенных инодов. В xfstests есть куча примеров использования ioctl XFS. Здесь — упрощенная программа, которая просто вызывает ioctl XFS_IOC_FSCOUNTS.

Давайте смонтируем тестовую файловую систему XFS для целей этой демонстрации.

$ truncate -s 20m xfs
$ mkfs.xfs ./xfs
$ mkdir /mnt/xfs
$ mount ./xfs /mnt/xfs -o loop

Хорошо, теперь у нас есть файловая система XFS, смонтированная в /mnt/xfs. Теперь вернемся к нашей программе fscounts, мы можем собрать ее и запустить в нашей тестовой файловой системе.

$ make
$ ./fscounts /mnt/xfs

Мы должны получить вывод, который выглядит примерно так:

XFS_IOC_FSCOUNTS-
    freedata: 3648 freertx: 0 freeino: 61 allocino: 64

Теперь давайте снова запустим программу, но с помощью strace, перехватывающей вызов ioctl.

$ strace -qq -e trace=ioctl ./fscounts /mnt/xfs
ioctl(3, 0xffffffff80205871, 0x7ffd9f059410) = 0
XFS_IOC_FSCOUNTS-
    freedata: 3648 freertx: 0 freeino: 61 allocino: 64

Опция -qq для strace подавляет дополнительный вывод, а -e trace=ioctl указывает strace распечатывать только информацию, относящуюся к системным вызовам ioctl. Итак, мы видим, что вызывается какой-то ioctl, но мало информации о том, что происходит с этим ioctl. Это связано с тем, что strace не понимает расположение структуры, связанной с этим ioctl. Некоторые другие ioctl могут быть декодированы, если их структура добавлена ​​в strace. Но многие ioctl файловой системы, такие как этот, просто отображаются как шестнадцатеричные аргументы для ioctl.

Так что же означают эти шестнадцатеричные числа? Если мы посмотрим на справочную страницу для ioctl, то увидим, что первый аргумент — это файловый дескриптор. Второй аргумент — это код запроса, зависящий от устройства. Третий аргумент — нетипизированный указатель на память.

Для кода запроса, зависящего от устройства, мы знаем из нашей тестовой программы, что мы передали значение «XFS_IOC_FSCOUNTS». Мы можем увидеть это определение в /usr/include/xfs/xfs_fs.h:

#define XFS_IOC_FSCOUNTS _IOR (‘X’, 113, struct xfs_fsop_counts)

Если мы посмотрим в исходный код ядра в asm-generic/ioctl.h, мы увидим определение _IOR:

#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))

Чуть выше мы видим, что _IOC определяется на основе направления (чтение/запись), типа, числа и размера. Младшие 16 бит — это тип и число (по 8 бит), в нашем случае это значения «X» и 113. Затем направление определяется с помощью макроса _IOR, устанавливающего это направление чтения. И, наконец, старшие 14 бит — это размер структуры, которая передается для следующего аргумента. И, конечно же, если мы напишем быструю программу на C для вывода значения XFS_IOC_FSCOUNTS, мы увидим, что это 0x80205871.

#include “xfs/xfs.h”
int main()
{
    printf(“XFS_IOC_FSCOUNTS 0x%x\n”, XFS_IOC_FSCOUNTS);
}
$ ./a.out
XFS_IOC_FSCOUNTS 0x80205871

И возвращаясь к выходным данным strace, мы видим, что это соответствует 32-битному значению второго аргумента:

ioctl(3, 0xffffffff80205871, 0x7ffd9f059410) = 0

Третий аргумент — это указатель памяти на структуру xfs_fsop_counts_t, определенную в /usr/include/xfs/xfs_fs.h как 4 64-битных значения без знака.

Итак, теперь у нас есть вся информация, необходимая для декодирования этого ioctl. Давайте посмотрим на Стратегию Лиз Райс в 60 строках на примере го. Мы видим, что код зацикливается на подпрограмме системного вызова ptrace, и если мы выходим из системного вызова, счетчик системного вызова увеличивается. Это идеальное место для внедрения нашего декодера ioctl.

Итак, сначала мы хотим увидеть, является ли системный вызов ioctl, в противном случае мы продолжим и попробуем еще раз следующий системный вызов. Мы можем проверить, является ли регистр Orig_rax syscall.SYS_IOCTL, и если это так, вызовем нашу функцию декодера.

if regs.Orig_rax == syscall.SYS_IOCTL {
    decodeIoctl(regs, pid)
}

Затем декодер может проверить конкретный ioctl, который мы хотим декодировать, просмотрев второй аргумент ioctl(), который хранится в regs.Rsi. Помните, что мы хотим смотреть только на младшие 32 бита, поэтому нам нужно преобразовать это в 32-битное целое число без знака. Если это не тот, который мы ищем, мы просто вернемся и попробуем еще раз:

const XFSIOCFSCOUNTS = 0x80205871
if uint32(regs.Rsi) != XFSIOCFSCOUNTS {
    return
}

Если это искомый ioctl, то мы можем декодировать 3-й аргумент, получив данные из области памяти, используя syscall.PtracePeekData(). Адрес памяти хранится в regs.Rdx, который является третьим аргументом ioctl(). Сначала нам нужно сделать байтовый срез размером со структуру xfs_fsop_counts_t.

type FsopCounts struct {
    FreeData uint64
    FreeRtX  uint64
    FreeIno  uint64
    AllocIno uint64
}
data := make([]byte, int(unsafe.Sizeof(FsopCounts{})))
_, err := syscall.PtracePeekData(pid, uintptr(regs.Rdx), data)

Затем мы можем использовать binary.Read() для заполнения нашей Go-версии структуры:

r := bytes.NewReader(data)
var f FsopCounts
err = binary.Read(r, binary.LittleEndian, &f)

И тогда все, что осталось, это распечатать структуру в stderr, как это сделал бы strace:

fmt.Fprintf(os.Stderr, “%+v\n”, f)

Теперь, если мы запустим нашу трассировку с помощью созданной выше тестовой программы fscounts, мы увидим, что теперь мы можем распечатать информацию о структуре в трассировке системного вызова ioctl:

% xfstrace ./fscounts /mnt/xfs
Run [./fscounts /mnt/xfs]
Wait returned: stop signal: trace/breakpoint trap
{FreeData:3648 FreeRtX:0 FreeIno:61 AllocIno:64}
XFS_IOC_FSCOUNTS-
    freedata: 3648 freertx: 0 freeino: 61 allocino: 64

Выходная строка: «{FreeData:3648 FreeRtX:0 FreeIno:61 AllocIno:64}» исходит из нашей программы трассировки при выходе из системного вызова ioctl.

Это можно распространить на любой ioctl, если мы знаем, какова структура памяти в третьем аргументе.