LINUX.ORG.RU

Вопрос по ассемблеру

 ,


3

1

хелловорлд на асме:

.section .data
msg:
.ascii "Hello, world!\n"
len = . - msg # символу len присваевается длина строки
.section .text
.global _start # точка входа в программу
_start:
movl $4, %eax # системный вызов № 4 — sys_write
movl $1, %ebx # поток № 1 — stdout
movl $msg, %ecx # указатель на выводимую строку
movl $len, %edx # длина строки
int $0x80 # вызов ядра
movl $1, %eax # системный вызов № 1 — sys_exit
xorl %ebx, %ebx # выход с кодом 0
int $0x80 # вызов ядра
взято отсюда https://habrahabr.ru/sandbox/26864/

Я вот не пойму, ассемблер — это ведь язык для процессора? Тогда откуда там взялись такие понятия как «системный вызов», «строка» «stdout» и и прочее? Про какое там «ядро» идет речь? Разве это не относится к системе, которая работает уже поверх процессора?

анонiмус взялся за ассемблер! ^_^ :3 :3

Тогда откуда там взялись такие понятия как «системный вызов», «строка» «stdout» и и прочее?

для удобства программиста

ассемблер — это ведь язык для процессора?

язык процессора - это машинный код, а ассемблер язык чуть уровнем выше, хотя бы потому, что в нём человекочитаемые названия инструкций и регистров

int $0x80 # вызов ядра

с точки зрения процессора это просто инструкция вызова прерывания с определённым номером, просто существует соглашение, что в 32 битном линуксе для x86 архитектуры системные вызовы реализуются именно таким образом

Harald ★★★★★ ()
Ответ на: комментарий от Harald

только вот еще что не ясно

«системный вызов», «строка» «stdout»
для удобства программиста

то что для удобства, это само-сабой. Но что означают эти слова в терминах асма, что они из себя представляют? Типы, или что?

linearisation ()

Системный вызов последовательность инструкций по занесению в нужные места параметров и вызова инструкции int, нужные места зависят от ОС, строка это потому что правильно называется макроассемблер, то есть зачатки языка высокого уровня присутствуют, stdout - просто число, тоже из ОС, есть ОС можешь пользоваться. Если голый процессор то нет никаких stdout и системных вызовов пока ты сам не их определишь.

ilovewindows ★★★★★ ()

Ты запихиваешь определённые значения (номер системного вызова, дескриптор файла stdout и т.д.) в регистры процессора. Затем передаёшь контроль коду ядра ОС, который выполняет ту или иную логику с этими значениями.

Deleted ()
Ответ на: комментарий от hateyoufeel

ну да, есть такая инструкция, рассказал, вот :)

Harald ★★★★★ ()

Разве это не относится к системе, которая работает уже поверх процессора?

Это как по вашему может выглядеть? Т.е. система это не машинный код, который исполняется процессором? Если не брать во внимание всякие случаи, когда некий абстрагированный от оборудования язык обрабатывается интерпретатором (а тут явно не такой случай), то любой код исполняется процессором, ну и в конечном итоге может быть представлен на ассемблере.

zloy_starper ★★★ ()

У Таненбаума в его Structured Computer Organization, уровень ассемблера лежит выше уровня операционной системы.

Evenik ()
Ответ на: комментарий от Evenik

это весьма странное мнение. Тут вопрос курицы и яйца возникать, вроде не должен: процессор может работать без ос, а вот ос без проца — навряд ли

linearisation ()
Ответ на: комментарий от linearisation

По int $0x80 происходит прерывание, то есть запоминается адрес инструкции, следующей за int, после чего процессор выполняет процедуру ядра ОС с теми параметрами, которые ты закладываешь в регистры. После выполнения этой процедуры происходит возврат в то место, адрес которого был ранее сохранён при прерывании, то есть обратно в твою программу, сразу за инструкцию int.

В x86_64 есть инструкция syscall, которая специально предназначена для системных вызовов. Адрес инструкции, следующей за syscall, запоминается в rcx и проиходит переход в процедуру, которая располагается по адресу, записанному в специальный регистр процессора. К этому регистру есть доступ только у ядра ОС. Что примечательно, номера системных вызовов при использовании инструкций int и syscall различаются. Так, тот же write будет под номером 1, если вызывать его через инструкцию syscall.

Строка - это просто набор байтов. То есть, если ты напишешь .string "blabla" или .ascii "blabla", то ассемблер создаст в указанной секции программы набор байтов, соответствующий этой строке. В твоём случае - это секция .data. После загрузки твоей программы эти данные окажутся в оперативной памяти. В системный вызов передаётся адрес первого байта этой строки и её длина. Ты можешь вывести содержимое секции программы выполнив objdump -s -j .data program_name в командной строке.

stdout - это просто число, соответствующее открытому «файлу», в который будет производится запись. Этот «файл» может быть обычным файлом, пайпом, tty, сокетом или ещё чем. При создании процесса для stdout всегда назначается число 1 и системный вызов write записывает в ассоциированный с этим числом «файл» данные, которые находятся по указанному тобой адресу.

anonymous ()

Ассемблер — программа, транслирующая язык ассемблера (ага, язык себя) в байт-код, исполняемый процессором.

Бинарные файлы как есть не исполняются процессором. В смысле те, которые у тебя в системе. У них есть заголовок и набор секций с кодом, с данными. Процессору всё равно что исполнять, а вот операционная система как раз и скажет, что именно.

По примеру:
.section .data
Это команда для ассемблера поместить то что дальше в секцию .data (данные) бинарного файла.

msg:
Это метка. Существует в исходном коде, после ассемблирования и линковки её уже не будет (разве что в отладочных символах, но это не в счёт, мы про то что процессор исполняет).

.ascii "Hello, world!\n"
.ascii — команда ассемблеру «щас будет строковый литерал».
Он возьмёт «Hello, world!\n» и как последовательность байт запишет в секцию .data.

len = . - msg # символу len присваевается длина строки
Опять же, игра с метками уровня компиляции, в бинарнике это не нужно.

.section .text
Объявление ассемблеру размещать следующие данные в секции кода, то есть их можно будет исполнять при работе. Этим и отличается от .data, можно прыгнуть к метке msg в программе, это корректно, но операционная система со своей виртуальной памятью скажет «это данные, их исполнять нельзя» и программа аварийно завершится.

.global _start # точка входа в программу
Объявление метки _start глобальной. Нужно для связывания.

_start:
Опять метка. В бинарнике в этом месте её не будет, адрес этой позиции будет в заголовке. Когда программа будет загружена в память, она будет запущена с этого места.

movl $4, %eax # системный вызов № 4 — sys_write
movl $1, %ebx # поток № 1 — stdout
movl $msg, %ecx # указатель на выводимую строку
movl $len, %edx # длина строки

Код. Транслируется, по сути, дословно. Вместо имён меток после связывания будут подставлены их адреса, их можно высчитывать на этапе компиляции.

int $0x80 # вызов ядра
Прерывание. В ядре есть механизм обработки прерываний. Если кратко и просто — процедура, адрес которой, условно, кладётся в определённое место. Когда случается прерывание (инструкция int), процессор запустит ту процедуру с максимальными привилегиями, аргументом передав ей 0x80.
Обработчик прерываний посмотрит, что за прерывание 0x80. Это «системный вызов». Конкретно в этом ядре. Совсем не обязательно чтобы так было. Но в линуксах так. Передаст управление обработчику системных вызовов. Он посмотрит в регистры, в них номер системного вызова. Будет передано управление ему. Тот посмотрит в другие регистры, где лежат аргументы для него, выполнит (или нет) то что надо, положит результат в %eax.

movl $1, %eax # системный вызов № 1 — sys_exit
xorl %ebx, %ebx # выход с кодом 0
int $0x80 # вызов ядра

То же самое. Вызов «exit» условно «завершит» программу, превратив её в зомби.

В общем, всё довольно просто.

evilface ★★ ()
Ответ на: комментарий от evilface

Этим и отличается от .data, можно прыгнуть к метке msg в программе, это корректно, но операционная система со своей виртуальной памятью скажет «это данные, их исполнять нельзя» и программа аварийно завершится.

То есть можно прыгать в любую секцию? А смысл, если это ошибка? как реализован механизм проверки корректности?

Вызов «exit» условно «завершит» программу, превратив её в зомби.

Почему в зомби?

anonymous ()
Ответ на: комментарий от anonymous

То есть можно прыгать в любую секцию? А смысл, если это ошибка? как реализован механизм проверки корректности?

В любую. Прыгать можно даже в не выделенную область памяти. Защита заключается в виртуальной памяти. Конкретно в страницах и в том, что у них есть права типа как у файлов. У страницы, куда содержимое .data поместят будут права «rw-», а у .text — «r-x». Проверяется в момент обращения по адресу, ибо там несколько уровней косвенности (архитектурно зависимо), MMU, адрес к котором мы обращаемся по ходу преобразования из виртуального в физический заодно будет проверен на корректность, на то, что страничку куда мы обращаемся не нужно выгрузить из swap и что она есть в принципе и на то, что то что мы делаем (читаем, пишем, исполняем) можно делать в принципе.

Подробней — гуглить про механизм виртуальной памяти (virtual memory).

Почему в зомби?

Чтобы можно было код завершения считать. Пока родитель не сделает wait(), он так и будет в списке процессов висеть.

evilface ★★ ()
Ответ на: комментарий от Evenik

это сравнение тёплого с мягким. ассемблер - это язык программирования (хотя и очень низкоуровневый). а ОС - это набор сервисных функций для обращения к системным ресурсам (в самом общем случае). так что ассемблер не ниже и не выше. это нельзя сравнивать. на ассемблере можно писать сразу без всякой оси код под процессор.

Iron_Bug ★★★★★ ()
Последнее исправление: Iron_Bug (всего исправлений: 1)
Ответ на: комментарий от evilface

будут права «rw-», а у .text — «r-x»

Что-то не получается прыгнуть в кучу и в дату. Как рулить этими правами ?

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int f(char *s, int n)
{
    return write(1, s, n);
}

char df[] = {   //
    0x55,       //                      push   %rbp
    0x48, 0x89, 0xe5,   //                mov    %rsp,%rbp
    0x48, 0x83, 0xec, 0x10,     //             sub    $0x10,%rsp
    0x48, 0x89, 0x7d, 0xf8,     //             mov    %rdi,-0x8(%rbp)
    0x89, 0x75, 0xf4,   //                mov    %esi,-0xc(%rbp)
    0x8b, 0x55, 0xf4,   //                mov    -0xc(%rbp),%edx
    0x48, 0x8b, 0x45, 0xf8,     //             mov    -0x8(%rbp),%rax
    0x48, 0x89, 0xc6,   //                mov    %rax,%rsi
    0xbf, 0x01, 0x00, 0x00, 0x00,       //          mov    $0x1,%edi
    0xb8, 0x00, 0x00, 0x00, 0x00,       //          mov    $0x0,%eax
    0xe8, 0x40, 0xfe, 0xff, 0xff,       //          callq  4004b0 <write@plt>
    0xc9,       //                      leaveq 
    0xc3,       //                      retq   
};

int main()
{
    int i, n = sizeof(df);
    int (*pf) (char *s, int n) = df;

    write(1, "a\n", 2);
    f("b\n", 2);
//    pf = malloc(n + 5);
//    memcpy(pf, df, n);
    pf("c\n", 2); // Segmentation fault
    return 0;
}

anonymous ()
Ответ на: комментарий от anonymous

«Можно прыгнуть» технически. Дальше, естественно, сегфолт. Из-за процитированной части сообщения.

По программе.
Во-первых, в чары толкать значения больше 127 не хорошо.
Во-вторых, такое приведение типа в C — UB. Не уверен, конечно, память может изменять, но вроде бы да.
В-третьих, по поводу как этим рулить. Можно выделить (man mmap(2)) анонимные страницы в памяти (не привязанные к файлам в ФС).

Как-то так:

void *code = mmap(NULL, size, PROT_WRITE | PROT_READ | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);

Дальше заполнить её, привести указатель к типу нужной функции и выполнять.

evilface ★★ ()

Доброе утро. Системные вызовы - это основа операционной системы, элементы через которые происходит общение разнообразных подсистем любой ОС. Их как правило насчитывается менее 300 штук. Более подробно можете почитать в сети.

charly_one ()
Ответ на: комментарий от I-Love-Microsoft

Доброе утро. Логическая операция на процессорах x86. Часто используется для обнуления регистра eax. xorl %eax,%eax. Запись нуля в аккумулятор - %eax. xor l - указывает на размер - 32 бита.

Данная команда весит на один байт больше, нежели команда movl $0,%eax.

charly_one ()
Ответ на: комментарий от evilface

Спасибо. С mmap такая ф-я ss - работает, а куча и дата - нет.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

char *ss(char *s)
{
    return s;
}

unsigned char df[] = {	//
    0x55,	//                      push   %rbp
    0x48, 0x89, 0xe5,	//                mov    %rsp,%rbp
    0x48, 0x89, 0x7d, 0xf8,	//             mov    %rdi,-0x8(%rbp)
    0x48, 0x8b, 0x45, 0xf8,	//             mov    -0x8(%rbp),%rax
    0x5d,	//                      pop    %rbp
    0xc3,	//                      retq   
};

int main()
{
    int n = sizeof(df);
    char *(*pf) (char *s) = NULL;

    write(1, ss("a\n"), 2);
    printf("d[%d]=%d\n", n, memcmp(ss, df, n));
    pf = mmap(NULL, n + 5, PROT_WRITE | PROT_READ | PROT_EXEC,
              MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    memcpy(pf, df, n);
    write(1, pf("b\n"), 2);
/*
    pf = malloc(n + 5);
    memcpy(pf, df, n);
    write(1, pf("c\n"), 2); // Segmentation fault
*/
/*
    pf = (void *) df;
    write(1, pf("d\n"), 2);	// Segmentation fault
*/
    return 0;
}

А при укладке в mmap-память предыдущей ф-и, вызывающей write - падает.

...
0xe8, 0x40, 0xfe, 0xff, 0xff,       //          callq  4004b0 <write@plt>
...
Эта строчка при разных компиляциях-запусках отличается от f(...) . (0x40 - 0x20)

anonymous ()
Ответ на: комментарий от anonymous

У меня опыта работы с подобным кодом почти нет. Но если скомпилить исходник с -Wall -Wextra -pedantic, gcc явно говорит, что в нём дофига чего сделано не по стандарту. Что-то мне подсказывает, что здесь придётся велосипедить с asm volatile и передачей параметров/прыжками оттуда.

evilface ★★ ()
Ответ на: комментарий от evilface

А. Ну так я вчитался в код. Там же в комментированном коде malloc(). Выкинь его, и всё работать будет. mmap() возвращает указатель на rwx память, а ты его затираешь. Во втором случае ещё очевидней — указатель на rwx память затирается указателем на df, который rw-.

evilface ★★ ()
Ответ на: комментарий от evilface

Закоментарены два блока про кучу и дату - это демки, как не получается. Они не запускались одновременно, только по очереди, один из трех: ммап, куча, дата.

anonymous ()
Ответ на: комментарий от evilface

Затирания не было.

С write-ом из mmap-а так поучается:

#include <unistd.h>
#include <string.h>
#include <sys/mman.h>

typedef int (*Tpfw) (int d, char *s, int n);

int f(Tpfw pfw, char *s, int n)
{
    return (*pfw) (1, s, n);
}

unsigned char df[] = {	//
    0x48, 0x89, 0xf8,	//                mov    %rdi,%rax
    0xbf, 0x01, 0x00, 0x00, 0x00,	//          mov    $0x1,%edi
    0xff, 0xe0,	//                   jmpq   *%rax
    0x66, 0x0f, 0x1f, 0x44, 0x00, 0x00,	//       nopw   0x0(%rax,%rax,1)
};

int main()
{
    int n = sizeof(df);
    int (*pf) (Tpfw pfw, char *s, int n);

    write(1, "a\n", 2);
    f(write, "b\n", 2);
    pf = mmap(NULL, n, PROT_WRITE | PROT_READ | PROT_EXEC,
              MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    memcpy(pf, df, n);
    pf(write, "c\n", 2);
    munmap(NULL, n);

    return 0;
}

anonymous ()
Ответ на: комментарий от anonymous

Ну в общем да. Только память ещё стоит высвободить munmap'ом нормально.

Вот с парой правок, так gcc и clang ругаются только на некорректные приведения указателей на функции к void*:

#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <stdlib.h>

typedef ssize_t (*Tpfw) (int d, const void *s, size_t n);

int f(Tpfw pfw, const void *s, size_t n)
{
    return (*pfw) (1, s, n);
}

unsigned char df[] = {
    0x48, 0x89, 0xf8,	                // mov    %rdi,%rax
    0xbf, 0x01, 0x00, 0x00, 0x00,	    // mov    $0x1,%edi
    0xff, 0xe0,	                        // jmpq   *%rax
    0x66, 0x0f, 0x1f, 0x44, 0x00, 0x00,	// nopw   0x0(%rax,%rax,1)
};

int main()
{
    size_t n = sizeof(df);
    ssize_t (*pf) (Tpfw pfw, const void *s, size_t n);

    write(1, "a\n", 2);
    f(write, "b\n", 2);
    pf = mmap(NULL, n, PROT_WRITE | PROT_READ | PROT_EXEC,
              MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    memcpy(pf, df, n);
    pf(write, "c\n", 2);
    munmap(pf, n);

    return 0;
}

evilface ★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.