Какова связь между (флагом переноса) и системным вызовом в сборке (синтаксис x64 Intel в Mac Os)?

Я новичок в языке ассемблера, и мне нужно реализовать read, используя язык ассемблера x64 в MAC. пока вот что я сделал:

;;;;;;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

как вы можете видеть выше, я вызываю чтение с системным вызовом, а затем сравниваю возвращаемое значение системного вызова чтения, хранящееся в rax, с 103, я объясню, почему я сравниваю его с 103, но перед этим позвольте мне объяснить еще кое-что, а именно errno (справочная страница Mac), вот что написано на странице руководства о errno:

Когда системный вызов обнаруживает ошибку, он возвращает целочисленное значение, указывающее на сбой (обычно -1), и соответствующим образом устанавливает переменную errno. ‹Это позволяет интерпретировать сбой при получении -1 и принять соответствующие меры.› Успешные вызовы никогда не устанавливают errno; после установки он остается до тех пор, пока не произойдет другая ошибка. Его следует проверять только после ошибки. Обратите внимание, что ряд системных вызовов перегружает значения этих номеров ошибок и что значения должны интерпретироваться в соответствии с типом и обстоятельствами вызова.

Ниже приведен полный список ошибок и их названий, приведенных в ‹sys/errno.h›.

0 Ошибка 0. Не используется.

1 EPERM Операция не разрешена. Предпринята попытка выполнить операцию, ограниченную процессами с соответствующими привилегиями или владельцем файла или других ресурсов.

2 ENOENT Нет такого файла или каталога. Компонент указанного пути не существует, или путь представляет собой пустую строку.

................................................. Я пропущу эту часть (кстати, я написал эту строку)...................................... .............

101 Тайм-аут ETIME STREAM ioctl(). Эта ошибка зарезервирована для использования в будущем.

102 EOPNOTSUPP Операция не поддерживается на сокете. Предпринятая операция не поддерживается для указанного типа сокета; например, пытаясь принять соединение через сокет дейтаграммы.

и, насколько я понимаю, и после того, как я много времени отлаживал с помощью lldb, я заметил, что syscall возвращает одно из тех чисел, которые показаны на странице руководства errno, например, когда я передаю неверный файловый дескриптор, в моей функции 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, потому что это то, что он должен делать.

Ну, все работает, как я и планировал, но есть проблема, которая возникла из ниоткуда, когда я открываю существующий файл и передаю его файловый дескриптор моей функции ft_read, как показано ниже main.c, наш read syscall возвращает "the number of bytes read is returned", это то, что read системный вызов возвращается, как описано в руководстве:

В случае успеха возвращается число прочитанных байтов (ноль указывает на конец файла), и позиция в файле увеличивается на это число. Если это число меньше запрошенного числа байтов, это не является ошибкой; это может произойти, например, из-за того, что сейчас доступно меньше байтов (может быть, из-за того, что мы были близки к концу файла, или из-за того, что мы читаем из конвейера или из терминала), или из-за того, что функция read() была прервана сигнал. См. также ПРИМЕЧАНИЯ.

При ошибке возвращается -1, и errno устанавливается соответствующим образом. В этом случае остается неуказанным, изменяется ли позиция в файле (если есть).

и в моем основном это работает довольно хорошо, я передаю своей функции ft_read хороший файловый дескриптор, буфер для хранения данных и 50 байт для чтения, поэтому 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

и угадайте, что это работает отлично, и errno возвращает 0, используя мой ft_read, когда нет ошибки, и возвращает соответствующий номер ошибки, когда есть ошибка.

но проблема в том, что я не знаю, почему устанавливается carry flag, когда нет cmp, устанавливает ли системный вызов carry flag, когда во время вызова возникает ошибка, или в фоновом режиме происходит что-то еще? Мне нужно подробное объяснение связи между системным вызовом и carry flag, я все еще новичок в ассемблере и очень хочу его изучить, и заранее спасибо.

Какова связь между syscall и carry 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);
}

person Holy semicolon    schedule 13.11.2020    source источник
comment
устанавливает ли системный вызов флаг переноса при возникновении ошибки во время вызова? Да. Он устанавливает CF в случае ошибки и очищает CF в случае успеха. Это соглашение о вызовах для конкретной сборки. Раньше было гораздо чаще. Есть даже инструкция stc и его противоположность clc, которые использовались рядом вызываемыми функциями для указания на ошибку или успех через флаг переноса. (Обработчики функций прерывания должны изменить кадр стека iret, чтобы установить или очистить CF вызывающей программы в стеке.)   -  person ecm    schedule 13.11.2020
comment
Имейте в виду, что системные вызовы, которые вы видите в man, являются оболочками реальных системных вызовов, выполняемых с помощью syscall. errno - это CRT. По крайней мере, так это работает в Linux, может быть и в Darwin. Системные вызовы Linux возвращают отрицательное значение при ошибке, и errno устанавливается из этих отрицательных значений. Вы должны проверить системный вызов Дарвина ABI, а не человека.   -  person Margaret Bloom    schedule 13.11.2020
comment
@MargaretBloom Спасибо за информацию, я проверил возвращаемое значение чтения системного вызова в системном вызове Дарвина из этого site и то же самое в возвращаемом значении.   -  person Holy semicolon    schedule 13.11.2020


Ответы (1)


Часть путаницы заключается в том, что термин системный вызов используется для двух вещей, которые действительно отличаются:

  1. Фактический запрос к ядру на чтение из файла, вызванный выполнением инструкции syscall.

  2. Функция C read(), предоставляемая библиотекой C пользовательского пространства как способ для программ C удобного доступа к функциям # 1.

На странице руководства описано, как использовать № 2, но в сборке вы работаете с № 1. Общая семантика одинакова, но детали того, как вы получаете к ним доступ, различаются.

В частности, функция C (#2) следует соглашению о том, что ошибки указываются путем возврата -1 из функции и установки переменной errno. Однако это не удобный способ для #1 указывать ошибки. errno — это глобальная (или локальная для потока) переменная, расположенная где-то в памяти программы; ядро не знает где, и было бы неловко сообщить об этом, поэтому ядро ​​не может легко записать эту переменную напрямую. Ядру проще возвращать коды ошибок каким-то другим способом и предоставить библиотеке C возможность установить переменную errno.

Соглашение, которому обычно следуют операционные системы на основе BSD, заключается в том, что системный вызов ядра (№ 1) устанавливает или очищает флаг переноса в зависимости от того, произошла ли ошибка. Если ошибки не произошло, rax содержит возвращаемое системным вызовом значение (здесь количество прочитанных байтов); если произошла ошибка, eax содержит код ошибки (обычно это 32-битное значение, поскольку errno является int). Поэтому, если вы пишете на ассемблере, вы должны ожидать увидеть именно это.

Что касается того, как ядру удается установить/очистить флаг переноса, когда системный вызов завершен, ядро ​​выполняет sysret инструкция по передаче управления обратно в пространство пользователя. Одной из функций этой инструкции является восстановление регистра rflags из r11. Ядро сохранит исходный rflags вашего процесса, когда начнется системный вызов, поэтому ему просто нужно установить или очистить младший бит (именно там находится флаг переноса) в этом 64-битном значении до или после загрузки его в r11 в подготовка к sysret. Затем, когда ваш процесс продолжит выполнение с инструкцией, следующей за вашим syscall, флаг переноса будет в соответствующем состоянии.

Инструкция cmp, безусловно, является одним способом, которым процессор x86 может установить флаг переноса, но ни в коем случае не единственным способом. И даже если бы это было так, вас не должно удивлять, что вы не видите этот код в программе пользовательского пространства, поскольку именно ядро ​​​​определяет, как он установлен.

Чтобы реализовать № 2, функция read() библиотеки C должна быть интерфейсом между соглашением ядра (# 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

Дополнительную информацию можно найти в документации по 64-разрядным системным вызовам для сборки MacOS. . Я хотел бы процитировать более авторитетную документацию, но я не знаю, где ее найти. То, что здесь, кажется, общеизвестно.

person Nate Eldredge    schedule 13.11.2020