; ============================================================
; fmv_unified.asm — GNU-compatible 'mv' command
; Builds with: nasm -f bin fmv_unified.asm -o fmv
;
; mv: Move (rename) files.
;
; Register allocation:
; r14d = argc, r15 = argv
; ebx = flags
; r13d = first non-option arg index
; ebp = exit code
;
; Flags in ebx:
; bit 0 = -f (force)
; bit 1 = -n (no-clobber)
; bit 2 = -v (verbose)
; bit 3 = -i (interactive — accepted but not impl)
; bit 4 = -T (no-target-directory)
; ============================================================
BITS 64
ORG 0x400000
%define SYS_READ 0
%define SYS_WRITE 1
%define SYS_OPEN 2
%define SYS_CLOSE 3
%define SYS_STAT 4
%define SYS_FSTAT 5
%define SYS_LSTAT 6
%define SYS_RT_SIGPROCMASK 14
%define SYS_EXIT 60
%define SYS_RENAME 82
%define SYS_UNLINK 87
%define SYS_RMDIR 84
%define SYS_MKDIR 83
%define SYS_GETDENTS64 217
%define SYS_COPY_FILE_RANGE 326
%define SYS_CHMOD 90
%define SYS_UTIMENSAT 280
%define STDOUT 1
%define STDERR 2
%define SIG_BLOCK 0
%define SIGPIPE 13
%define O_RDONLY 0
%define O_WRONLY 1
%define O_CREAT 64
%define O_TRUNC 512
%define O_DIRECTORY 0x10000
; stat structure offsets
%define STAT_MODE 24
%define STAT_SIZE 48
%define S_IFMT 0xF000
%define S_IFDIR 0x4000
%define S_IFLNK 0xA000
%define S_IFREG 0x8000
; errno values
%define EPERM 1
%define ENOENT 2
%define EIO 5
%define EACCES 13
%define EBUSY 16
%define EEXIST 17
%define EXDEV 18
%define ENOTDIR 20
%define EISDIR 21
%define EINVAL 22
%define ENOSPC 28
%define EROFS 30
%define ENOMEM 12
%define ENOTEMPTY 39
%define PATH_MAX 4096
%define COPY_BUF_SIZE 65536
; --- ELF Header (64 bytes) ---
ehdr:
db 0x7f, 'E','L','F'
db 2, 1, 1, 0
dq 0
dw 2, 0x3e
dd 1
dq _start
dq phdr - $$
dq 0
dd 0
dw 64, 56, 2, 64, 0, 0
; --- Program Header: PT_LOAD ---
phdr:
dd 1, 7
dq 0, $$, $$, file_size, file_size, 0x200000
; --- Program Header: PT_GNU_STACK (NX) ---
dd 0x6474e551, 6
dq 0, 0, 0, 0, 0, 0x10
; ============================================================
; Code
; ============================================================
_start:
; Block SIGPIPE
sub rsp, 16
mov qword [rsp], 0
bts qword [rsp], SIGPIPE
mov eax, SYS_RT_SIGPROCMASK
mov edi, SIG_BLOCK
mov rsi, rsp
xor edx, edx
mov r10d, 8
syscall
add rsp, 16
; Save argc/argv
mov r14d, [rsp]
lea r15, [rsp + 8]
xor ebx, ebx
xor ebp, ebp
mov ecx, 1
; Parse options
.parse_opts:
cmp ecx, r14d
jge .opts_exhausted
mov rdi, [r15 + rcx*8]
cmp byte [rdi], '-'
jne .done_opts
cmp byte [rdi + 1], 0
je .done_opts
cmp byte [rdi + 1], '-'
je .check_long
; Short options
inc rdi
.short_loop:
movzx eax, byte [rdi]
test al, al
jz .next_opt
cmp al, 'f'
je .set_force
cmp al, 'n'
je .set_noclobber
cmp al, 'v'
je .set_verbose
cmp al, 'i'
je .set_interactive
cmp al, 'T'
je .set_no_target_dir
; Invalid option
push rcx
push rdi
mov rsi, str_prefix
mov edx, str_prefix_len
call do_write_err
mov rsi, str_invalid
mov edx, str_invalid_len
call do_write_err
pop rdi
mov rsi, rdi
mov edx, 1
call do_write_err
mov rsi, str_sq_nl
mov edx, 2
call do_write_err
mov rsi, str_try
mov edx, str_try_len
call do_write_err
pop rcx
mov edi, 1
jmp do_exit
.set_force:
or bl, 1
; -f cancels -n -i
and bl, ~2
and bl, ~8
inc rdi
jmp .short_loop
.set_noclobber:
or bl, 2
; -n cancels -f -i
and bl, ~1
and bl, ~8
inc rdi
jmp .short_loop
.set_verbose:
or bl, 4
inc rdi
jmp .short_loop
.set_interactive:
or bl, 8
and bl, ~1
and bl, ~2
inc rdi
jmp .short_loop
.set_no_target_dir:
or bl, 16
inc rdi
jmp .short_loop
.check_long:
cmp byte [rdi + 2], 0
je .double_dash
mov r13, rdi
push rcx
mov rsi, str_help_flag
call str_eq
test eax, eax
jnz .pop_show_help
mov rdi, r13
mov rsi, str_version_flag
call str_eq
test eax, eax
jnz .pop_show_version
mov rdi, r13
mov rsi, str_force_flag
call str_eq
test eax, eax
jnz .pop_set_force
mov rdi, r13
mov rsi, str_noclobber_flag
call str_eq
test eax, eax
jnz .pop_set_noclobber
mov rdi, r13
mov rsi, str_verbose_flag
call str_eq
test eax, eax
jnz .pop_set_verbose
mov rdi, r13
mov rsi, str_no_target_dir_flag
call str_eq
test eax, eax
jnz .pop_set_no_target_dir
; Unrecognized
mov rsi, str_prefix
mov edx, str_prefix_len
call do_write_err
mov rsi, str_unrecog
mov edx, str_unrecog_len
call do_write_err
mov rdi, r13
call str_len
mov edx, eax
mov rsi, r13
call do_write_err
mov rsi, str_sq_nl
mov edx, 2
call do_write_err
mov rsi, str_try
mov edx, str_try_len
call do_write_err
pop rcx
mov edi, 1
jmp do_exit
.pop_show_help:
pop rcx
jmp .show_help
.pop_show_version:
pop rcx
jmp .show_version
.pop_set_force:
pop rcx
or bl, 1
and bl, ~2
and bl, ~8
jmp .next_opt
.pop_set_noclobber:
pop rcx
or bl, 2
and bl, ~1
and bl, ~8
jmp .next_opt
.pop_set_verbose:
pop rcx
or bl, 4
jmp .next_opt
.pop_set_no_target_dir:
pop rcx
or bl, 16
jmp .next_opt
.double_dash:
inc ecx
jmp .done_opts
.next_opt:
inc ecx
jmp .parse_opts
.opts_exhausted:
.done_opts:
mov r13d, ecx
; Count non-option args
mov eax, r14d
sub eax, r13d
cmp eax, 0
jle .err_missing_operand
cmp eax, 1
je .err_missing_dest
; 2 args: mv SRC DEST
cmp eax, 2
je .two_args
; Multi args: mv SRC... DIR
jmp .multi_args
.two_args:
mov rdi, [r15 + r13*8] ; source
lea eax, [r13d + 1]
mov rsi, [r15 + rax*8] ; dest
; Check if -T: treat dest as file
test bl, 16
jnz .two_direct
; Check if dest is directory
push rdi
push rsi
sub rsp, 152
mov rdi, rsi
mov rsi, rsp
mov eax, SYS_STAT
syscall
test rax, rax
js .two_not_dir
mov eax, [rsp + STAT_MODE]
and eax, S_IFMT
cmp eax, S_IFDIR
je .two_is_dir
.two_not_dir:
add rsp, 152
pop rsi
pop rdi
jmp .two_direct
.two_is_dir:
add rsp, 152
pop r12 ; dest dir
pop rdi ; source
; Build dest = dir/basename(source)
push rdi
push r12
call basename
mov r8, rax
sub rsp, PATH_MAX
mov rdi, rsp
mov rsi, [rsp + PATH_MAX] ; dir
.td_copy_dir:
lodsb
test al, al
jz .td_dir_done
stosb
jmp .td_copy_dir
.td_dir_done:
mov byte [rdi], '/'
inc rdi
mov rsi, r8
.td_copy_bname:
lodsb
stosb
test al, al
jnz .td_copy_bname
mov rsi, rsp
mov rdi, [rsp + PATH_MAX + 8] ; source
call do_move
add rsp, PATH_MAX
pop r12
pop rdi
jmp .exit_done
.two_direct:
call do_move
jmp .exit_done
.multi_args:
; mv SRC... DIR — last arg is directory
mov eax, r14d
dec eax
mov r12, [r15 + rax*8]
; Stat to confirm directory
sub rsp, 152
mov rdi, r12
mov rsi, rsp
mov eax, SYS_STAT
syscall
test rax, rax
js .multi_not_dir
mov eax, [rsp + STAT_MODE]
and eax, S_IFMT
cmp eax, S_IFDIR
jne .multi_not_dir
add rsp, 152
mov r8d, r13d
.multi_loop:
lea eax, [r14d - 1]
cmp r8d, eax
jge .exit_done
mov rdi, [r15 + r8*8]
push r8
call basename
mov r9, rax
sub rsp, PATH_MAX
mov rdi, rsp
mov rsi, r12
.mc_dir:
lodsb
test al, al
jz .mc_dir_done
stosb
jmp .mc_dir
.mc_dir_done:
mov byte [rdi], '/'
inc rdi
mov rsi, r9
.mc_bname:
lodsb
stosb
test al, al
jnz .mc_bname
mov r8, [rsp + PATH_MAX]
mov rdi, [r15 + r8*8]
mov rsi, rsp
call do_move
add rsp, PATH_MAX
pop r8
inc r8d
jmp .multi_loop
.multi_not_dir:
add rsp, 152
mov rsi, str_prefix
mov edx, str_prefix_len
call do_write_err
mov rsi, str_target_str
mov edx, str_target_str_len
call do_write_err
mov rdi, r12
call str_len
mov edx, eax
mov rsi, r12
call do_write_err
mov rsi, str_not_a_dir
mov edx, str_not_a_dir_len
call do_write_err
mov ebp, 1
jmp .exit_done
.exit_done:
mov edi, ebp
jmp do_exit
.show_help:
mov edi, STDOUT
mov rsi, str_help
mov edx, str_help_len
call do_write
xor edi, edi
jmp do_exit
.show_version:
mov edi, STDOUT
mov rsi, str_version
mov edx, str_version_len
call do_write
xor edi, edi
jmp do_exit
.err_missing_operand:
mov rsi, str_prefix
mov edx, str_prefix_len
call do_write_err
mov rsi, str_missing
mov edx, str_missing_len
call do_write_err
mov rsi, str_try
mov edx, str_try_len
call do_write_err
mov edi, 1
jmp do_exit
.err_missing_dest:
mov rsi, str_prefix
mov edx, str_prefix_len
call do_write_err
mov rsi, str_missing_dest
mov edx, str_missing_dest_len
call do_write_err
mov rdi, [r15 + r13*8]
call str_len
mov edx, eax
mov rsi, [r15 + r13*8]
call do_write_err
mov rsi, str_sq_nl
mov edx, 2
call do_write_err
mov rsi, str_try
mov edx, str_try_len
call do_write_err
mov edi, 1
jmp do_exit
; ============================================================
; do_move: move source to dest
; Input: rdi = source, rsi = dest
; Uses: ebx = flags, ebp = exit code
; ============================================================
do_move:
push r12
push r13
push r14
mov r12, rdi ; source
mov r13, rsi ; dest
mov r14d, ebx
; If -n (no-clobber): check if dest exists
test r14d, 2
jz .dm_no_noclobber
sub rsp, 152
mov rdi, r13
mov rsi, rsp
mov eax, SYS_LSTAT
syscall
add rsp, 152
test rax, rax
jns .dm_done ; dest exists, skip (no-clobber)
.dm_no_noclobber:
; Try rename first
mov rdi, r12
mov rsi, r13
mov eax, SYS_RENAME
syscall
test rax, rax
jz .dm_rename_ok
neg rax
; If EXDEV, need to copy+unlink
cmp eax, EXDEV
je .dm_cross_device
; Other error
push rax
jmp .dm_report_error
.dm_rename_ok:
; Verbose
test r14d, 4
jz .dm_done
mov rsi, str_renamed
mov edx, str_renamed_len
call do_write_err
mov rdi, r12
call str_len
mov edx, eax
mov rsi, r12
call do_write_err
mov rsi, str_arrow
mov edx, str_arrow_len
call do_write_err
mov rdi, r13
call str_len
mov edx, eax
mov rsi, r13
call do_write_err
mov rsi, str_sq_nl
mov edx, 2
call do_write_err
jmp .dm_done
.dm_cross_device:
; Cross-device: open source, create dest, copy, close, unlink source
; Open source for reading
mov rdi, r12
mov esi, O_RDONLY
xor edx, edx
mov eax, SYS_OPEN
syscall
test rax, rax
js .dm_open_src_err
push rax ; save src fd
; fstat source to get mode and size
sub rsp, 152
mov edi, eax
mov rsi, rsp
mov eax, SYS_FSTAT
syscall
mov r8d, [rsp + STAT_MODE] ; mode
mov r9, [rsp + STAT_SIZE] ; size
add rsp, 152
; Create dest
mov rdi, r13
mov esi, O_WRONLY | O_CREAT | O_TRUNC
mov edx, r8d
and edx, 0o7777
mov eax, SYS_OPEN
syscall
test rax, rax
js .dm_open_dst_err
push rax ; save dst fd
; Copy data using read/write loop
; Allocate buffer on stack
sub rsp, COPY_BUF_SIZE
.dm_copy_loop:
; read from source
mov eax, SYS_READ
mov edi, [rsp + COPY_BUF_SIZE + 8] ; src fd
mov rsi, rsp
mov edx, COPY_BUF_SIZE
syscall
test rax, rax
jle .dm_copy_done ; 0 = EOF, negative = error
; write to dest
mov rdx, rax ; bytes to write
mov eax, SYS_WRITE
mov edi, [rsp + COPY_BUF_SIZE] ; dst fd
mov rsi, rsp
syscall
; Simplified: assume full write. For robustness, should loop.
jmp .dm_copy_loop
.dm_copy_done:
add rsp, COPY_BUF_SIZE
; Close dst
pop rdi ; dst fd
push rdi
mov edi, edi
mov eax, SYS_CLOSE
syscall
pop rdi
; Close src
pop rdi ; src fd
push rdi
mov edi, edi
mov eax, SYS_CLOSE
syscall
pop rdi
; Unlink source
mov rdi, r12
mov eax, SYS_UNLINK
syscall
; Verbose
test r14d, 4
jz .dm_done
mov rsi, str_renamed
mov edx, str_renamed_len
call do_write_err
mov rdi, r12
call str_len
mov edx, eax
mov rsi, r12
call do_write_err
mov rsi, str_arrow
mov edx, str_arrow_len
call do_write_err
mov rdi, r13
call str_len
mov edx, eax
mov rsi, r13
call do_write_err
mov rsi, str_sq_nl
mov edx, 2
call do_write_err
jmp .dm_done
.dm_open_src_err:
neg rax
push rax
jmp .dm_report_error
.dm_open_dst_err:
neg rax
push rax
; Close src fd
pop rdi
push rdi
mov edi, [rsp + 8] ; src fd
mov eax, SYS_CLOSE
syscall
pop rax ; get errno back
pop rdi ; discard src fd
push rax
jmp .dm_report_error
.dm_report_error:
mov ebp, 1
mov rsi, str_prefix
mov edx, str_prefix_len
call do_write_err
mov rsi, str_cannot_mv
mov edx, str_cannot_mv_len
call do_write_err
mov rdi, r12
call str_len
mov edx, eax
mov rsi, r12
call do_write_err
mov rsi, str_to_sq
mov edx, str_to_sq_len
call do_write_err
mov rdi, r13
call str_len
mov edx, eax
mov rsi, r13
call do_write_err
mov rsi, str_colon_space
mov edx, str_colon_space_len
call do_write_err
pop rdi
call errno_to_msg
call do_write_err
mov rsi, str_newline
mov edx, 1
call do_write_err
.dm_done:
pop r14
pop r13
pop r12
ret
; ============================================================
; basename: get basename of path
; Input: rdi = path
; Output: rax = pointer to basename
; ============================================================
basename:
push rdi
call str_len
mov ecx, eax
pop rdi
test ecx, ecx
jz .bn_start
dec ecx
.bn_strip:
cmp ecx, 0
jl .bn_start
cmp byte [rdi + rcx], '/'
jne .bn_scan
dec ecx
jmp .bn_strip
.bn_scan:
mov eax, ecx
.bn_find:
cmp eax, 0
jl .bn_start
cmp byte [rdi + rax], '/'
je .bn_found
dec eax
jmp .bn_find
.bn_found:
inc eax
lea rax, [rdi + rax]
ret
.bn_start:
mov rax, rdi
ret
; ============================================================
; errno_to_msg
; ============================================================
errno_to_msg:
cmp edi, EPERM
je .e_eperm
cmp edi, ENOENT
je .e_enoent
cmp edi, EIO
je .e_eio
cmp edi, EACCES
je .e_eacces
cmp edi, EBUSY
je .e_ebusy
cmp edi, EEXIST
je .e_eexist
cmp edi, EXDEV
je .e_exdev
cmp edi, ENOTDIR
je .e_enotdir
cmp edi, EISDIR
je .e_eisdir
cmp edi, EINVAL
je .e_einval
cmp edi, ENOSPC
je .e_enospc
cmp edi, EROFS
je .e_erofs
cmp edi, ENOTEMPTY
je .e_enotempty
mov rsi, str_err_unknown
mov edx, str_err_unknown_len
ret
.e_eperm:
mov rsi, str_err_eperm
mov edx, str_err_eperm_len
ret
.e_enoent:
mov rsi, str_err_enoent
mov edx, str_err_enoent_len
ret
.e_eio:
mov rsi, str_err_eio
mov edx, str_err_eio_len
ret
.e_eacces:
mov rsi, str_err_eacces
mov edx, str_err_eacces_len
ret
.e_ebusy:
mov rsi, str_err_ebusy
mov edx, str_err_ebusy_len
ret
.e_eexist:
mov rsi, str_err_eexist
mov edx, str_err_eexist_len
ret
.e_exdev:
mov rsi, str_err_exdev
mov edx, str_err_exdev_len
ret
.e_enotdir:
mov rsi, str_err_enotdir
mov edx, str_err_enotdir_len
ret
.e_eisdir:
mov rsi, str_err_eisdir
mov edx, str_err_eisdir_len
ret
.e_einval:
mov rsi, str_err_einval
mov edx, str_err_einval_len
ret
.e_enospc:
mov rsi, str_err_enospc
mov edx, str_err_enospc_len
ret
.e_erofs:
mov rsi, str_err_erofs
mov edx, str_err_erofs_len
ret
.e_enotempty:
mov rsi, str_err_enotempty
mov edx, str_err_enotempty_len
ret
; ============================================================
; Utility functions
; ============================================================
do_write:
mov eax, SYS_WRITE
syscall
cmp rax, -4
je do_write
ret
do_write_err:
mov edi, STDERR
jmp do_write
do_exit:
mov eax, SYS_EXIT
syscall
str_len:
xor eax, eax
.sl_loop:
cmp byte [rdi + rax], 0
je .sl_done
inc eax
jmp .sl_loop
.sl_done:
ret
str_eq:
xor r8d, r8d
.se_loop:
movzx eax, byte [rdi + r8]
movzx edx, byte [rsi + r8]
cmp al, dl
jne .se_ne
test al, al
jz .se_eq
inc r8d
jmp .se_loop
.se_eq:
mov eax, 1
ret
.se_ne:
xor eax, eax
ret
; ============================================================
; Data
; ============================================================
; @@DATA_START@@
str_help:
db "Usage: mv [OPTION]... [-T] SOURCE DEST", 10
db " or: mv [OPTION]... SOURCE... DIRECTORY", 10
db " or: mv [OPTION]... -t DIRECTORY SOURCE...", 10
db "Rename SOURCE to DEST, or move SOURCE(s) to DIRECTORY.", 10, 10
db "Mandatory arguments to long options are mandatory for short options too.", 10
db " -f, --force do not prompt before overwriting", 10
db " -i, --interactive prompt before overwrite", 10
db " -n, --no-clobber do not overwrite an existing file", 10
db " -T, --no-target-directory treat DEST as a normal file", 10
db " -v, --verbose explain what is being done", 10
db " --help display this help and exit", 10
db " --version output version information and exit", 10, 10
db "GNU coreutils online help: <https://www.gnu.org/software/coreutils/>", 10
db "Full documentation <https://www.gnu.org/software/coreutils/mv>", 10
db "or available locally via: info '(coreutils) mv invocation'", 10
str_help_len equ $ - str_help
str_version:
db "mv (GNU coreutils) 9.4", 10
db "Copyright (C) 2023 Free Software Foundation, Inc.", 10
db "License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.", 10
db "This is free software: you are free to change and redistribute it.", 10
db "There is NO WARRANTY, to the extent permitted by law.", 10, 10
db "Written by Mike Parker, David MacKenzie, and Jim Meyering.", 10
str_version_len equ $ - str_version
str_prefix: db "mv: "
str_prefix_len equ $ - str_prefix
str_invalid: db "invalid option -- '"
str_invalid_len equ $ - str_invalid
str_unrecog: db "unrecognized option '"
str_unrecog_len equ $ - str_unrecog
str_missing: db "missing file operand", 10
str_missing_len equ $ - str_missing
str_missing_dest: db "missing destination file operand after '"
str_missing_dest_len equ $ - str_missing_dest
str_sq_nl: db "'", 10
str_try: db "Try 'mv --help' for more information.", 10
str_try_len equ $ - str_try
str_cannot_mv: db "cannot move '"
str_cannot_mv_len equ $ - str_cannot_mv
str_to_sq: db "' to '"
str_to_sq_len equ $ - str_to_sq
str_colon_space: db "': "
str_colon_space_len equ $ - str_colon_space
str_renamed: db "renamed '"
str_renamed_len equ $ - str_renamed
str_arrow: db "' -> '"
str_arrow_len equ $ - str_arrow
str_target_str: db "target '"
str_target_str_len equ $ - str_target_str
str_not_a_dir: db "' is not a directory", 10
str_not_a_dir_len equ $ - str_not_a_dir
; @@DATA_END@@
; Error messages
str_err_eperm: db "Operation not permitted"
str_err_eperm_len equ $ - str_err_eperm
str_err_enoent: db "No such file or directory"
str_err_enoent_len equ $ - str_err_enoent
str_err_eio: db "Input/output error"
str_err_eio_len equ $ - str_err_eio
str_err_eacces: db "Permission denied"
str_err_eacces_len equ $ - str_err_eacces
str_err_ebusy: db "Device or resource busy"
str_err_ebusy_len equ $ - str_err_ebusy
str_err_eexist: db "File exists"
str_err_eexist_len equ $ - str_err_eexist
str_err_exdev: db "Invalid cross-device link"
str_err_exdev_len equ $ - str_err_exdev
str_err_enotdir: db "Not a directory"
str_err_enotdir_len equ $ - str_err_enotdir
str_err_eisdir: db "Is a directory"
str_err_eisdir_len equ $ - str_err_eisdir
str_err_einval: db "Invalid argument"
str_err_einval_len equ $ - str_err_einval
str_err_enospc: db "No space left on device"
str_err_enospc_len equ $ - str_err_enospc
str_err_erofs: db "Read-only file system"
str_err_erofs_len equ $ - str_err_erofs
str_err_enotempty: db "Directory not empty"
str_err_enotempty_len equ $ - str_err_enotempty
str_err_unknown: db "Unknown error"
str_err_unknown_len equ $ - str_err_unknown
str_newline: db 10
str_help_flag: db "--help", 0
str_version_flag: db "--version", 0
str_force_flag: db "--force", 0
str_noclobber_flag: db "--no-clobber", 0
str_verbose_flag: db "--verbose", 0
str_no_target_dir_flag: db "--no-target-directory", 0
file_size equ $ - $$