在汇编语言中(Mac Os上的x64 Intel语法),(进位标志)和syscall之间有什么关系?

4

我是汇编语言的新手,我需要使用汇编语言x64MAC上实现read函数。 到目前为止,这是我做的:

;;;;;;ft_read.s;;;;;;

global _ft_read:
section .text
extern ___error

_ft_read:
    mov rax, 0x2000003 ; store syscall value of read on rax 
    syscall            ; call read and pass to it rdi , rsi, rdx  ==> rax read(rdi, rsi, rdx)
    cmp rax, 103       ; compare rax with 103 by subtracting 103 from rax ==> rax - 103
    jl _ft_read_error  ; if the result of cmp is less than 0 then jump to _ft_read_error
    ret                ; else return the rax value which is btw the return value of syscall

_ft_read_error:
    push rax
    call ___error
    pop rcx
    mov [rax], rcx
    mov rax, -1
    ret

如上所述,我使用系统调用 syscall 来调用 read 函数,然后将 syscall 返回的 rax 存储的值与 103 进行比较。我将在解释为什么要与 103 进行比较之前,解释一些其他内容,即关于 errno(mac 的 man 手册),以下是有关 errno 的手册页面中的描述:

当系统调用检测到错误时,它会返回一个整数值,表示失败(通常为 -1),并相应地设置变量 errno。

以下是错误及其名称的完整列表,如 <sys/errno.h> 中所示。

0 错误 0。未使用。

1 EPERM 不允许操作。尝试执行仅限于具有适当权限的进程或文件或其他资源所有者的操作。

2 ENOENT 没有此文件或目录。指定路径名的某个组成部分不存在,或该路径名为空字符串。

..................................................我将跳过此部分(顺便说一下,这行是我写的)..................................................

101 ETIME 流 ioctl() 超时。此错误保留供将来使用。

102 EOPNOTSUPP 不支持套接字上的操作。所引用套接字类型不支持尝试的操作;例如,在数据报套接字上尝试接受连接。

经过我调试了很长时间,使用 lldb 我注意到,syscall 返回的数字是在 errno man 手册中显示的数字之一,例如,当我传递一个坏的文件描述符时,在我的 ft_read 函数中使用以下 main.c 代码:

int bad_file_des = -1337;// a file descriptor which it doesn't exist of course, you can change it with -42 as you like
ft_read(bad_file_des, buff, 300);

我们的syscall返回值为9,存储在rax中,因此我要比较rax<103(因为errno值从0到102),然后跳转到ft_read_error,因为这是应该做的。

好的,一切都按照我计划的进行,但是突然出现了一个问题,当我打开一个现有文件并将它的文件描述符传递给下面的main.c中所示的ft_read函数时,我们的readsyscall返回值为"返回读取的字节数",这就是手册中描述的readsyscall返回值:

成功时,返回读取的字节数(零表示文件结束),文件位置将向前移动此数字。如果此数字小于请求的字节数,则不会发生错误;例如,可能由于当前实际上可用的字节较少(可能是因为我们接近文件末尾或者因为我们正在从管道或终端读取)或者read()被信号中断而发生这种情况。也请参见NOTES。

出错时返回-1,errno相应设置。在这种情况下,文件位置(如有)是否更改是未指定的。

在我的主程序中,当我将一个好的文件描述符、一个缓冲区以及50个字节的数据传递给我的ft_read函数时,它的工作非常顺利。因此,syscall将返回50并存储在rax中,然后比较完成了其任务>> rax=50 < 103,因此它会跳转到ft_read_error,即使没有错误,只是因为50是这些不适用于此案例的errno错误号之一。

有人建议使用jc(设置进位标志后跳转)而不是jl(如果小于则跳转),如下所示:

;;;;;;ft_read.s;;;;;;

global _ft_read:
section .text
extern ___error

_ft_read:
    mov rax, 0x2000003 ; store syscall value of read on rax 
    syscall            ; call read and pass to it rdi , rsi, rdx  ==> rax read(rdi, rsi, rdx)
                       ; deleted the cmp
    jc _ft_read_error  ; if carry flag is set then jump to _ft_read_error
    ret                ; else return the rax value which is btw the return value of syscall

_ft_read_error:
    push rax
    call ___error
    pop rcx
    mov [rax], rcx
    mov rax, -1
    ret

猜猜怎么着,当没有错误时使用我的ft_read,它能完美运行并返回0errno,而当有错误时返回相应的错误号码。

但问题在于,当没有cmp时,我不知道为什么carry flag被设置了,是syscall在调用期间发生错误时设置了carry flag,还是背景中发生了其他事情?我想要详细解释syscall和carry flag之间的关系,我还是新手汇编,非常渴望学习,提前感谢。

syscallcarry flag之间的关系以及syscall如何设置它?

这是我使用来编译上面汇编代码的main.c函数:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>

ssize_t ft_read(int fildes, void *buf, size_t nbyte);

int     main()
{
    /*-----------------------------------------------------------------------*/
    ///////////////////////////////////////////////////////////////////////////
    /********************************ft_read**********************************/
    int     fd = open("./main.c", O_RDONLY);
    char    *buff = calloc(sizeof(char), 50 + 1);
    int     ret = ft_read(fd, buff, 50);

    printf("ret value = %d,  error value = %d : %s\n", ret, errno, strerror(errno));
    //don't forget to free ur buffer bro, this is just a test main don't be like me.
    return (0);
}

3
当系统调用出错时,syscall会设置进位标志(CF),调用成功时会清除进位标志。这是一种汇编特定的调用约定,以前更为常见。甚至有指令stc和它的相反指令clc被近距离调用的函数用来通过进位标志指示错误或成功。(中断函数处理程序需要修改iret堆栈帧,在堆栈上设置或清除调用者的CF。) - ecm
2
请注意,您在man中看到的系统调用只是对使用syscall进行的真实系统调用的包装器。 errno是CRT的一部分。至少在Linux下是这样工作的,可能在Darwin下也是如此。 Linux的系统调用在出错时返回负值,并从这些负值设置errno。 您应该检查Darwin的系统调用ABI而不是man。 - Margaret Bloom
@MargaretBloom 感谢您提供的信息,我查看了这个网站中Darwin系统调用read的返回值,并且它与返回值相同。 - Holy semicolon
1个回答

6
一部分混淆在于术语“系统调用”被用于两个实际上不同的事物:
1. 由执行syscall指令所调用的向内核请求从文件读取的实际请求。 2. 由用户空间C库提供的C函数read(),作为C程序方便地访问第1点功能的方式。
手册记录了如何使用第2点,但是在汇编中你正在使用第1点。总体语义相同,但是访问它们的细节是不同的。
特别地,C函数(第2点)遵循错误通过从函数返回-1并设置变量errno来指示的约定。然而,这对于第1点来说不是一个方便的指示错误的方式。errno是位于程序内存中某个位置的全局(或线程本地)变量;内核不知道它在哪里,告诉它会很尴尬,因此内核不能直接写入该变量。内核更简单的方法是以其他方式返回错误代码,并留给C库来设置errno变量。
BSD系操作系统通常遵循的惯例是内核系统调用(#1)将根据是否发生错误设置或清除进位标志。如果没有发生错误, rax 包含系统调用的返回值(这里是读取的字节数);如果发生错误, eax 包含错误代码(通常是32位值,因为 errno 是一个 int )。因此,如果您使用汇编语言编写代码,那么您应该期望看到这种情况。
关于内核如何设置/清除进位标志,当系统调用完成时,内核执行sysret指令将控制权转移回用户空间。这个指令的一个功能是从r11中恢复rflags寄存器。在系统调用开始时,内核会保存您的进程原始的rflags,因此它只需在加载到sysret之前或之后在这个64位值中设置或清除低位比特(这就是进位标志的位置)。然后当您的进程继续执行syscall后面的指令时,进位标志将处于相应的状态。 cmp指令确实是x86 CPU设置进位标志的一种方式,但这并不是唯一的方式。即使它是唯一的方式,您也不应该感到惊讶,因为内核确定了它的设置方式,所以在用户空间程序中看不到这些代码。
为了实现#2,C库的read()函数需要在内核约定(#1)和C程序员期望的(#2)之间进行接口交互,因此他们必须编写一些代码来检查进位标志并在需要时填充errno。他们用于此功能的代码可能如下所示:
    global read
read:
    mov rax, 0x2000003
    ; fd, buf, count are in rdi, rsi, rdx respectively
    syscall
    jc read_error
    ; no error, return value is in rax which is where the C caller expects it
    ret
read_error:
    ; error occurred, eax contains error code
    mov [errno], eax
    ; C caller expects return value of -1
    mov rax, -1 
    ret

MacOS汇编64位系统调用文档中有更多信息。我希望能引用更权威的文档,但我不知道在哪里找到它。这里提供的似乎是“常识”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接