Which linux system call is used by ls command in linux to display the folder/file name?
Asked Answered
A

3

24

I wanted to know which system call is used in linux by the ls command to display the folder's (or file's name)? Especially the files/folders starting with "." (dot)

I executed the strace ls -a command to look at the system calls.There is a lot of fstat calls which occur for the all the other attributes (inode to permisisons). Which one actually gives it's name?

execve("/bin/ls", ["ls", "-a"], [/* 37 vars */]) = 0
brk(0)                                  = 0x1762000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1c14025000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=109464, ...}) = 0
mmap(NULL, 109464, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f1c1400a000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20T\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=121936, ...}) = 0
mmap(NULL, 2221680, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1c13be6000
mprotect(0x7f1c13c03000, 2093056, PROT_NONE) = 0
mmap(0x7f1c13e02000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c000) = 0x7f1c13e02000
mmap(0x7f1c13e04000, 1648, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f1c13e04000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/librt.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340!\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=31752, ...}) = 0
mmap(NULL, 2128984, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1c139de000
mprotect(0x7f1c139e5000, 2093056, PROT_NONE) = 0
mmap(0x7f1c13be4000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6000) = 0x7f1c13be4000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libacl.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\33\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=31096, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1c14009000
mmap(NULL, 2126312, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1c137d6000
mprotect(0x7f1c137dd000, 2093056, PROT_NONE) = 0
mmap(0x7f1c139dc000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6000) = 0x7f1c139dc000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\30\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1811128, ...}) = 0
mmap(NULL, 3925208, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1c13417000
mprotect(0x7f1c135cc000, 2093056, PROT_NONE) = 0
mmap(0x7f1c137cb000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b4000) = 0x7f1c137cb000
mmap(0x7f1c137d1000, 17624, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f1c137d1000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340\r\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=14768, ...}) = 0
mmap(NULL, 2109704, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1c13213000
mprotect(0x7f1c13215000, 2097152, PROT_NONE) = 0
mmap(0x7f1c13415000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f1c13415000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200l\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=135366, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1c14008000
mmap(NULL, 2212904, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1c12ff6000
mprotect(0x7f1c1300e000, 2093056, PROT_NONE) = 0
mmap(0x7f1c1320d000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x17000) = 0x7f1c1320d000
mmap(0x7f1c1320f000, 13352, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f1c1320f000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libattr.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340\17\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=18552, ...}) = 0
mmap(NULL, 2113736, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1c12df1000
mprotect(0x7f1c12df5000, 2093056, PROT_NONE) = 0
mmap(0x7f1c12ff4000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f1c12ff4000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1c14007000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1c14005000
arch_prctl(ARCH_SET_FS, 0x7f1c140057c0) = 0
mprotect(0x7f1c137cb000, 16384, PROT_READ) = 0
mprotect(0x7f1c12ff4000, 4096, PROT_READ) = 0
mprotect(0x7f1c1320d000, 4096, PROT_READ) = 0
mprotect(0x7f1c13415000, 4096, PROT_READ) = 0
mprotect(0x7f1c139dc000, 4096, PROT_READ) = 0
mprotect(0x7f1c13be4000, 4096, PROT_READ) = 0
mprotect(0x7f1c13e02000, 4096, PROT_READ) = 0
mprotect(0x618000, 4096, PROT_READ)     = 0
mprotect(0x7f1c14027000, 4096, PROT_READ) = 0
munmap(0x7f1c1400a000, 109464)          = 0
set_tid_address(0x7f1c14005a90)         = 4490
set_robust_list(0x7f1c14005aa0, 0x18)   = 0
futex(0x7fff2e09d99c, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 1, NULL, 7f1c140057c0) = -1 EAGAIN (Resource temporarily unavailable)
rt_sigaction(SIGRTMIN, {0x7f1c12ffc750, [], SA_RESTORER|SA_SIGINFO, 0x7f1c13005cb0}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {0x7f1c12ffc7e0, [], SA_RESTORER|SA_RESTART|SA_SIGINFO, 0x7f1c13005cb0}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
getrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM_INFINITY}) = 0
statfs("/selinux", {f_type="EXT2_SUPER_MAGIC", f_bsize=4096, f_blocks=238304997, f_bfree=232078639, f_bavail=219973436, f_files=60530688, f_ffree=60120220, f_fsid={-1173666966, -474985328}, f_namelen=255, f_frsize=4096}) = 0
brk(0)                                  = 0x1762000
brk(0x1783000)                          = 0x1783000
open("/proc/filesystems", O_RDONLY)     = 3
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1c14024000
read(3, "nodev\tsysfs\nnodev\trootfs\nnodev\tb"..., 1024) = 328
read(3, "", 1024)                       = 0
close(3)                                = 0
munmap(0x7f1c14024000, 4096)            = 0
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=7220736, ...}) = 0
mmap(NULL, 7220736, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f1c1270e000
close(3)                                = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=41, ws_col=144, ws_xpixel=0, ws_ypixel=0}) = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
getdents(3, /* 2 entries */, 32768)     = 48
getdents(3, /* 0 entries */, 32768)     = 0
close(3)                                = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1c14024000
write(1, ".  ..\n", 6.  ..
)                  = 6
close(1)                                = 0
munmap(0x7f1c14024000, 4096)            = 0
close(2)                                = 0
exit_group(0)                           = ?
Abdul answered 16/10, 2012 at 18:19 Comment(0)
G
43

Most of the system calls there are noise from loading shared libraries at startup. The interesting things happen here:

openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
getdents(3, /* 2 entries */, 32768)     = 48
getdents(3, /* 0 entries */, 32768)     = 0
close(3)  

The openat(2) system call is used to open the current directory (".") relative to the current working directory (the AT_FDCWD flag). The O_DIRECTORY flag indicates that it wants to open the directory and read the directory's contents.

The actual directory data is read using the getdents(2) system call. In this case, it called it twice, since until it returns 0, it's not sure if there's more data or not. Finally, the file descriptor is closed after it's done.

If you were to write your own program, however, you wouldn't call these directly -- instead you'd use opendir(3), readdir(3), and closedir(3) to read a directory. They're portable (POSIX-compliant), and they insulate you from the details of the underlying system calls. They're also easier to use, IMO.

Goldina answered 16/10, 2012 at 18:29 Comment(0)
C
0

it is the stat(2) syscall which query the status of some file (perhaps a directory); if you have a file descriptor (after an open(2) syscall) you can use fstat(2)

To read a directory, you'll better use the readdir(3) function which calls the getdents(2) syscall.

Cursive answered 16/10, 2012 at 18:23 Comment(0)
O
0

More verbose strace dump

Prevent strace from abbreviating arguments? teaches us how:

strace -v -s 99999 ls

If we create some distinctly named test files on an empty directory:

touch aaaaaaaaaaaaaaaaaaaa
touch bbbbbbbbbbbbbbbbbbbb
touch cccccccccccccccccccc

and now search for aaaaaaaaaaaaaaaaaaaa on the output we see the following two hits (getdents64 manually indented by me):

getdents64(3, [
    {d_ino=8328002, d_off=1, d_reclen=24, d_type=DT_DIR, d_name="."},
    {d_ino=1536544, d_off=2, d_reclen=24, d_type=DT_DIR, d_name=".."},
    {d_ino=8329332, d_off=358524250, d_reclen=40, d_type=DT_REG, d_name="cccccccccccccccccccc"},
    {d_ino=8329330, d_off=496322106, d_reclen=40, d_type=DT_REG, d_name="aaaaaaaaaaaaaaaaaaaa"},
    {d_ino=8329756, d_off=497914259, d_reclen=40, d_type=DT_REG, d_name="bbbbbbbbbbbbbbbbbbbb"}
], 131072) = 168

write(1, "aaaaaaaaaaaaaaaaaaaa  bbbbbbbbbbbbbbbbbbbb  cccccccccccccccccccc\n", 65aaaaaaaaaaaaaaaaaaaa  bbbbbbbbbbbbbbbbbbbb  cccccccccccccccccccc

We know that he write of course writes it to stdout, so it seems very likely that getdents64 is the one getting the names. Besides the three files we created, we also see that the special . and .. are also always present at the syscall level. Cool stuff.

man getdents64 then confirms our suspicion. That page notably has a minimal sample code that heps quench any doubts.

The first argument of getdents64 was 3, which is a common number for the first file opened by a program (since 0, 1 and 2 are taken by stdin, stdout and stderr by default).

By searching above the getdents64 for the number 3 we see:

close(3)                                = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
newfstatat(3, "", {st_dev=makedev(0, 0x27), st_ino=8328002, st_mode=S_IFDIR|0775, st_nlink=2, st_uid=1000, st_gid=1000, st_blksize=131072, st_blocks=18, st_size=5, st_atime=1665092404 /* 2022-10-06T22:40:04.155914251+0100 */, st_atime_nsec=155914251, st_mtime=1665092401 /* 2022-10-06T22:40:01.415910918+0100 */, st_mtime_nsec=415910918, st_ctime=1665092401 /* 2022-10-06T22:40:01.415910918+0100 */, st_ctime_nsec=415910918}, AT_EMPTY_PATH) = 0
getdents64(3,

so we understand that:

  • something else had 3 open, but then closed it
  • openat returned our 3 with input argument ".", since we implicitly listed the current working directory with ls
  • newfstatat was used to get some metadata from it
  • getdents64 then used that descriptor

Static source code analysis

Let's have some fun to hopefully confirm the stuff other answers said, i.e.:

  • getdents is the base syscall, or more precisely, getdents64 on an x86_64 system like mine
  • opendir and readdir are convenience helpers

GNU Coreutils is the most popular desktop implementation of ls, and the one I have on my Ubuntu 22.04 computer right now. We can find this with:

apt-file search '/usr/bin/ls'

and we know that it is at version 8.32 according to dpkg -l | grep coreutils.

Here's the file that implements it on a GitHub mirror: https://github.com/coreutils/coreutils/blob/v8.32/src/ls.c#L2972 slightly summarized:

DIR *dirp;
struct dirent *next;
dirp = opendir (name);
while (1) {
    next = readdir (dirp);
    total_blocks += gobble_file (next->d_name, type,
                                RELIABLE_D_INO (next),
                                false, name);

man readdir tells us that next->d_name is the basename of the entry.

gobble_file then goes on to keep a list of entries, which is then printed at the end. This presumably has to be done because readdir does not return a deterministic order, and ls wants to print an ordered file list.

The functions opendir and readdir are implemented in most desktop systems in the glibc, which on my Ubuntu 22.04 is at version 2.35 according to dpkg -l | grep libc6.

readir is implemented at: https://github.com/bminor/glibc/blob/glibc-2.35/sysdeps/unix/sysv/linux/readdir.c#L42

struct dirent *
__readdir_unlocked (DIR *dirp) {
      bytes = __getdents (dirp->fd, dirp->data, maxread);
}
weak_alias (__readdir, readdir)

__getdents is likely implemented at https://github.com/bminor/glibc/blob/glibc-2.35/sysdeps/unix/sysv/linux/getdents64.c

ssize_t
__getdents64 (int fd, void *buf, size_t nbytes)
{
  /* The system call takes an unsigned int argument, and some length
     checks in the kernel use an int type.  */
  if (nbytes > INT_MAX)
    nbytes = INT_MAX;
  return INLINE_SYSCALL_CALL (getdents64, fd, buf, nbytes);
}
libc_hidden_def (__getdents64)
weak_alias (__getdents64, getdents64)

INLINE_SYSCALL_CALL is defined at https://github.com/bminor/glibc/blob/glibc-2.35/sysdeps/unix/sysdep.h and likely (it is an infinitely deep sea of macros) ends up calling the x86_64 assembly syscall helper at https://github.com/bminor/glibc/blob/glibc-2.35/sysdeps/unix/sysv/linux/x86_64/syscall.S#L29 which places syscall arguments one by one on the correct registers.

ENTRY (syscall)
    movq %rdi, %rax     /* Syscall number -> rax.  */
    movq %rsi, %rdi     /* shift arg1 - arg5.  */
    movq %rdx, %rsi
    movq %rcx, %rdx
    movq %r8, %r10
    movq %r9, %r8
    movq 8(%rsp),%r9    /* arg6 is on the stack.  */
    syscall         /* Do the system call.  */
    cmpq $-4095, %rax   /* Check %rax for error.  */
    jae SYSCALL_ERROR_LABEL /* Jump to error handler if error.  */
    ret         /* Return to caller.  */

some more comments: What are the calling conventions for UNIX & Linux system calls (and user-space functions) on i386 and x86-64

And the syscall number itself appears to come from https://github.com/bminor/glibc/blob/glibc-2.35/sysdeps/unix/sysv/linux/x86_64/64/arch-syscall.h#L90

#define __NR_getdents64 217

Finally on the Linux kernel syscall table at https://github.com/torvalds/linux/blob/v5.15/arch/x86/entry/syscalls/syscall_64.tbl#L228 we can confirm that the syscall 217 is the one for getdents64:

217 common  getdents64      sys_getdents64

Step debug ls with debug symbols

On Ubuntu 22.04 as per https://askubuntu.com/questions/487222/how-to-install-debug-symbols-for-installed-packages

# Get debug symbols.
printf "deb http://ddebs.ubuntu.com %s main restricted universe multiverse\n" $(lsb_release -cs){,-updates,-security,-proposed} | \
 sudo tee -a /etc/apt/sources.list.d/ddebs.list
sudo apt install ubuntu-dbgsym-keyring
sudo apt update
sudo apt install coreutils-dbgsym

# Get source.
apt source coreutils

# Alternative less precise way to get sources.
# git clone --depth 1 --branch v8.32 https://github.com/coreutils/coreutils

# Run ls pointing to the downloaded source.
gdb -ex 'set substitute-path . /home/ciro/git/coreutils/coreutils-8.32/' ls

Upon starting on main:

start

we are at src/ls.c as expected.

We try:

break readdir
continue

but only to notice that we don't have glibc sources!

../sysdeps/unix/sysv/linux/readdir64.c: No such file or directory.

So we get them:

apt source libc6

and then on GDB:

set substitute-path .. /home/ciro/git/coreutils/glibc-2.35

We use .. here to match the weird ../sysdeps/unix/sysv/linux/readdir64.c with which symbols were compiled.

OK, so now we are at sysdeps/unix/sysv/linux/readdir64.c:

struct dirent64 *
__readdir64 (DIR *dirp)

And so on.

Ovariotomy answered 6/10, 2022 at 21:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.