; INVOKATION:
; inotify /name0/name1/.../name_to_look_for
; RETURN VALUES:
; 0 if found, an error number if not. See end of this file for specifics.
; to build, an example using nasm:
; nasm -f elf64 inotify.asm
; ld -s inotify.o
; I tried to avoid as much as possible the use of nasm preprocessor (super
; powerful). Worse case scenario, use preprocessor features which are ez to
; implement (not the powerful features with high technical cost of
; implementation for obvious reasons). To help, I used the equ instruction
; instead of a preprocessor macro definition. That to allow ez porting to any
; other/future intel syntax assembler.
; notes:
; - for a string, the 'size' is the whole including the terminating null char
; - for a string, the 'length' is the number of actual char without the
; terminating null char
; This uses a blocking file descriptor with a select syscall for timeout
; estimation. Should not use select (that binary array passing is weird),
; should not use a blocking file descriptor.
; the kernel copies only a set of whole events in the read buffer, with the
; the name padded to the hdr size with zeros.
; At the time of writing, linux has a default max count of fds which is 1024
; and, on x86_64, a stack sz limit of 8192 bytes, those per userland task.
; you need x86_64 ABI document from AMD
; you need x86_64 ISA doc, from Intel or AMD.
[section .data]
;******************************************************************************
; the converted linux uapi header
;/*
; * struct inotify_event - structure read from the inotify device for each event
; *
; * When you are watching a directory, you will receive the filename for events
; * such as IN_CREATE, IN_DELETE, IN_OPEN, IN_CLOSE, ..., relative to the wd.
; */
inotify_event_watch_desc: equ 0 ;s32 /* watch descriptor */
inotify_event_mask: equ 4 ;u32 /* watch mask */
inotify_event_cookie: equ 8 ;u32 /* cookie to synchronize two events */
; XXX: the event name + terminating null char has its size zeros padded on
; inotify_event_hdr_sz!
; (data padding is common for linux user exchanged data)
inotify_event_name_padded_sz: equ 12 ;u32 /* length (including nulls) of name */
inotify_event_hdr_sz: equ 16
inotify_event_name: equ 16 ;char name[] /* stub for possible name */
;/* the following are legal, implemented events that user-space can watch for */
IN_ACCESS: equ 0x00000001 ;/* File was accessed */
IN_MODIFY: equ 0x00000002 ;/* File was modified */
IN_ATTRIB: equ 0x00000004 ;/* Metadata changed */
IN_CLOSE_WRITE: equ 0x00000008 ;/* Writtable file was closed */
IN_CLOSE_NOWRITE: equ 0x00000010 ;/* Unwrittable file closed */
IN_OPEN: equ 0x00000020 ;/* File was opened */
IN_MOVED_FROM: equ 0x00000040 ;/* File was moved from X */
IN_MOVED_TO: equ 0x00000080 ;/* File was moved to Y */
IN_CREATE: equ 0x00000100 ;/* Subfile was created */
IN_DELETE: equ 0x00000200 ;/* Subfile was deleted */
IN_DELETE_SELF: equ 0x00000400 ;/* Self was deleted */
IN_MOVE_SELF: equ 0x00000800 ;/* Self was moved */
;/* the following are legal events. they are sent as needed to any watch */
IN_UNMOUNT: equ 0x00002000 ;/* Backing fs was unmounted */
IN_Q_OVERFLOW: equ 0x00004000 ;/* Event queued overflowed */
IN_IGNORED: equ 0x00008000 ;/* File was ignored */
;/* helper events */
IN_CLOSE: equ (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE) ;/* close */
IN_MOVE: equ (IN_MOVED_FROM | IN_MOVED_TO) ;/* moves */
;/* special flags */
IN_ONLYDIR: equ 0x01000000 ;/* only watch the path if it is a directory */
IN_DONT_FOLLOW: equ 0x02000000 ;/* don't follow a sym link */
IN_EXCL_UNLINK: equ 0x04000000 ;/* exclude events on unlinked objects */
IN_MASK_ADD: equ 0x20000000 ;/* add to the mask of an already existing watch */
IN_ISDIR: equ 0x40000000 ;/* event occurred against dir */
IN_ONESHOT: equ 0x80000000 ;/* only send event once */
;/*
; * All of the events - we build the list by hand so that we can add flags in
; * the future and not break backward compatibility. Apps will get only the
; * events that they originally wanted. Be sure to add new events here!
; */
IN_ALL_EVENTS: equ (IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE | \
IN_CLOSE_NOWRITE | IN_OPEN | IN_MOVED_FROM | \
IN_MOVED_TO | IN_DELETE | IN_CREATE | IN_DELETE_SELF | \
IN_MOVE_SELF)
;/* Flags for sys_inotify_init1. */
; XXX:linux is reaching common flag values for all archs!
O_CLOEXEC: equ 0o2000000
O_NONBLOCK: equ 0o0004000
IN_CLOEXEC: equ O_CLOEXEC
IN_NONBLOCK: equ O_NONBLOCK
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
; the linux syscall ABI on x86_64
; syscall number is in rax register
; syscall parameters in the following ordered registers:
; rdi
; rsi
; rdx
; r10
; r8
; r9
; syscalls clobbers r11, rcx and rax, cause of sysret(syscall) machine instruction
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
; input:s32 fd, u64 ptr to buf, u64 count
; output:s64 count
sysc_read: equ 0
;-------------------------------------------------------------------------------
; input:s32 fd, u64 ptr to buf, u64 count
; output:s64 count
sysc_write: equ 1
;-------------------------------------------------------------------------------
; input:s32 flags
; output:s32 fd
sysc_inotify_init1: equ 294
;-------------------------------------------------------------------------------
; input:s32 fd, u64 ptr to pathname, u32 mask
sysc_inotify_add_watch: equ 254
;-------------------------------------------------------------------------------
; input:s32 exit code
sysc_exit_group: equ 231
;-------------------------------------------------------------------------------
; input: s32 max fd + 1, u64 ptr on array of read rdy fd bits, u64 ptr on array
; of write rdy fd bits, u64 ptr on array of special rdy fd bits, u64 ptr
; on timeval(tv_sec,tv_usec) timeout
; output:s32 exit code(-EINTR...) and proper fd bits set in their respective
; array
sysc_select: equ 23
;-------------------------------------------------------------------------------
;******************************************************************************
;******************************************************************************
;------------------------------------------------------------------------------
; XXX: wrong way of doing this. The right way, query the limits of the mounted
; file system mounted there. _Usually_ this limit is 256 bytes.
path_sz_max: equ 1024
;------------------------------------------------------------------------------
time_out: equ 4 ; 4 seconds is huge, should be passed on the command line
events_max equ 1024 ; process a maximum of 1024 events before bailing out, should be passed on the command line
;------------------------------------------------------------------------------
eintr: equ 4
errno_max: equ 4095
;===============================================================================
; to reach more readability and ease register manipulation in the main function,
; use the stack for storage
path_start: equ 8*0 ; args[1]
path_null: equ 8*1
path_last_name_separator: equ 8*2 ; ptr on last '/' (separator of path names) of path
path_last_name_start: equ 8*3
path_last_name_len equ 8*4
;-------------------------------------------------------------------------------
inotify_fd: equ 8*5
;-------------------------------------------------------------------------------
fd_set_read: equ 8*6 ; 64 fds is fairly enough. On linux x86_64, max default is 1024 bits for select fds per process. You could do more since ulimits file_max is dynamic, but you should not with select because you may end up passing huge bits array
timeval_tv_sec: equ 8*7
timeval_tv_usec: equ 8*8
;-------------------------------------------------------------------------------
events_remaining: equ 8*9 ; count to limit the number of processed events
;-------------------------------------------------------------------------------
inotify_event_buf: equ 8*10 ; 512 bytes for the read buffer (should accomodate really nasty filesystems already)
inotify_event_buf_sz: equ 8*64
;-------------------------------------------------------------------------------
start_stack_room: equ 8*10 + inotify_event_buf_sz
;===============================================================================
;******************************************************************************
;******************************************************************************
[section .text]
; _start is the ELF symbol which defines the program entry point
[global _start:function]
_start:
mov rax, [rsp] ; x86_64 ABI->argc
cmp rax,1 ; we have always the name of the program, then we want more than 1 argument
je .exit_no_error
mov rbx, [rsp + 2 * 8] ; x86_64 ABI->ptr in stack on array of arg ptrs, here rbx = args[1]
; make some room on the stack
sub rsp, start_stack_room
mov [rsp + path_start], rbx
;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
; rbx = ptr on path start
;-------------------------------------------------------------------------------
; forward lookup for the the terminating null character which ends the file path
cld
xor al,al ; null character
mov rdi, [rsp + path_start]
mov rcx, path_sz_max
repne scasb ; the flags are from the cmp operation, not from counter termination condition
jne .exit_path_too_big
dec rdi ; rewind 1 byte to point on the terminating null character
cmp rdi, [rsp + path_start] ; empty path, we exit here but the following code should not presume a non-empty path
je .exit_no_error
mov [rsp + path_null], rdi
;-------------------------------------------------------------------------------
; backward lookup for the last '\' separator for path names
std
mov al, '/'
mov rdi, [rsp + path_null]
dec rdi ; point on the char right before the terminating null char
mov rcx, [rsp + path_null]
sub rcx, [rsp + path_start] ; n chars to compare, len of path (= ptr on terminating null char - ptr on first char)
repne scasb ; the flags are from the cmp operation, not from counter termination condition
jne .exit_no_path_separator
inc rdi ; forward 1 byte to point on '/'
mov [rsp + path_last_name_separator], rdi
;-------------------------------------------------------------------------------
mov rax, sysc_inotify_init1
; arg 0
xor rdi, rdi
syscall
cmp rax, -errno_max
jnb .exit_inotify_init_failed
mov [rsp + inotify_fd], rax
;-------------------------------------------------------------------------------
mov rax, sysc_inotify_add_watch
; arg 0
mov rdi, [rsp + inotify_fd]
; arg 1
mov rbx, [rsp + path_last_name_separator]
mov rsi, [rsp + path_start]
cmp rbx, rsi
je .file_at_root
mov byte [rbx], 0 ; do nullify the '/' if not at root
.file_at_root:
; arg 2
mov rdx, IN_CREATE|IN_ONLYDIR
syscall
cmp rax, -errno_max
jnb .exit_inotify_add_watch_failed
;===============================================================================
; Here start the main loop, we have a event reader and an event scanner.
; Based on linux code, only whole events are read, and there is no short read.
; Linux rounds each event size is a modulo of inotify_event_hdr size, zerop
; added.
mov rax, [rsp + path_last_name_separator]
inc rax
mov [rsp + path_last_name_start], rax
mov rbx, [rsp + path_null]
sub rbx, rax ; len of path last name
mov [rsp + path_last_name_len], rbx ; _not_ including the terminating null char, we use len not size
mov qword [rsp + events_remaining], events_max ; if the file system is really active in the watched dir, it can last a very long time despite the time out, cap the number of processed events
; 4 sec timeout granularity, we do not reset it each time we call linux select to wait the same amount of time whatever happens
mov qword [rsp + timeval_tv_sec], time_out
mov qword [rsp + timeval_tv_usec], 0
.select:
; zero the read fd_set, and set the inotify fd bit
mov rcx, [rsp + inotify_fd]
mov qword [rsp + fd_set_read], 1
shl qword [rsp + fd_set_read], cl ; 8 bins cl is enough because the value of our file destriptor is very low
mov rax, sysc_select
mov rdi, [rsp + inotify_fd]
inc rdi ; max fd + 1
lea rsi, [rsp + fd_set_read]
xor rdx, rdx
xor r10, r10
lea r8, [rsp + timeval_tv_sec]
syscall
cmp rax, -eintr ; got a signal, don't care and keep going
je .select
cmp rax, -errno_max
jnb .exit_select_failed
test rax, rax
jz .exit_select_timedout
; since we are expecting only 1 type of event from select, it must be our inotify events
;-------------------------------------------------------------------------------
.read:
mov rax, sysc_read
mov rdi, [rsp + inotify_fd]
lea rsi, [rsp + inotify_event_buf]
mov rdx, inotify_event_buf_sz
syscall
cmp rax, -eintr ; a signal was received before anything was read, don't care then restart
je .read
; error, something is really wrong, bail out
cmp rax, -errno_max
jnb .exit_inotify_read_failed
;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
; rax = the number of read bytes
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
; next is the scanner of the read events
;-------------------------------------------------------------------------------
.scan:
mov r8, rax ; the right above number of read bytes
lea r9, [rsp + inotify_event_buf]
mov r10, [rsp + path_last_name_start]
mov r11, [rsp + path_last_name_len]
cld ; move forward in strings
.scan_loop:
xor r15, r15
mov r15d, [r9 + inotify_event_name_padded_sz]
test r15, r15
jz .next_event
.event_have_name:
; look for the terminating null char
xor al, al ; '\0'
; r14 = rdi = inotify_event_name_start
mov r14, r9
add r14, inotify_event_hdr_sz
mov rdi, r14
mov rcx, r15 ; don't go beyond the event name upper bound
repne scasb
jne .exit_event_name_is_corrupted ; no null char? fishy fishy, bail out
dec rdi ; rewind rdi one byte to point on terminator null char
sub rdi, r14 ; (ptr on terminating null char - ptr on first char) = str len, _not_ str sz
cmp rdi, r11
jne .next_event
; cmp the names
mov rsi, r14 ; inotify_event_name_start
mov rdi, r10 ; path_last_name_start
mov rcx, r11 ; inotify_event_name_len = path_last_name_len
repe cmpsb
jne .next_event
; **** FOUND *****
jmp .exit_no_error
.next_event:
dec qword [rsp + events_remaining]
jz .exit_too_many_events
sub r8, inotify_event_hdr_sz
sub r8, r15
jz .select
add r9, inotify_event_hdr_sz
add r9, r15
jmp .scan_loop
;******************************************************************************
.exit_no_error:
xor edi, edi ; ret = 0
jmp .exit_group
;******************************************************************************
;******************************************************************************
.exit_path_too_big:
mov edi, 1 ; ret = 1
jmp .exit_group
;******************************************************************************
.exit_no_path_separator:
mov edi, 2 ; ret = 2
jmp .exit_group
;******************************************************************************
.exit_inotify_init_failed:
mov edi, 3 ; ret = 3
jmp .exit_group
;******************************************************************************
.exit_inotify_add_watch_failed:
mov edi, 4 ; ret = 4
jmp .exit_group
;******************************************************************************
.exit_select_failed:
mov edi, 5 ; ret = 5
jmp .exit_group
;******************************************************************************
.exit_select_timedout:
mov edi, 6 ; ret = 6
jmp .exit_group
;******************************************************************************
.exit_inotify_read_failed:
mov edi, 7 ; ret = 7
jmp .exit_group
;******************************************************************************
.exit_event_name_is_corrupted:
mov edi, 8 ; ret = 8
jmp .exit_group
;******************************************************************************
.exit_too_many_events:
mov edi, 9 ; ret = 9
jmp .exit_group
;******************************************************************************
;******************************************************************************
.exit_group:
mov rax, sysc_exit_group
syscall ; never returning syscall
;******************************************************************************