LINUX.ORG.RU

Gas, stack, Ассемблерная вставка

 


2

3

здрасте, здрасте люди добрые!

есть процедура

.global proc
proc:

push %rbp
mov %rsp, %rbp

sub 64, %rbp

mov $777, -24(%rbp)




pop %rbp
mov %rbp, %rsp



ret

подскажите, как данные из стека, теперь выудить в программе на С++?


#include <iostream>

int main()
{

asm(

"call proc\n"

// в каждой новой программе под стек выделяется новая область в оперативной памяти, я так понимаю.  подскажите пожалуйста, как теперь можно обратиться к тому стеку, что в процедуре  proc? 

через ss?  как достать от туда 777? 
очень надеюсь, что поможете
);


}

подскажите, как данные из стека, теперь выудить в программе на С++?

Для начала учти выравнивание стека при вызове функции. А вообще, не парь череп и запусти в дебагере и сам посмотри. Выравнивание может всё поменять. Так что в асме тоже выравнивай. Чтобы было как и в трой функции. И, аргументы, про них не забудь. Они тоже могут на стек леч перед вызовом.

anonymous ()

подскажите, как данные из стека, теперь выудить в программе на С++?

У тебя делжна быть не инициализированная переменная в функции указывающая на те 777. Т.е. тебе нужно напихать переменных, чтобы подвинуть стек после вызова. И потом ещё одну. И смотри, чтобы компиль не повыкидывал.

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

Если я его правильно понял, то у него процедура ассемблерная в одном файле(file.s) и вней он создает стек, а сишная программа в которой он хочет извлечь данные из стека, в другом файле (c.cpp) потом он их видими вместе скомпилит и хочет получить данные из стека.

Я понимаю что кроме анонима, ему тут никто не поможет. Ассемблер тут непопулярен.

LGH ()

Тебе надо думать в обратном направлении - как из ассемблерной функции передать данные в вызывающий С код. А для этого тебе надо знать и следовать C ABI.

Begemoth ★★★★★ ()
Файлы
  • main.c
  • set.s
main.c

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

uint64_t read ();
void     set ();

int
main() {

	uint64_t val = 0;

	set ();
	val = read ();

	printf ("%ld was set\n", val);

	return EXIT_SUCCESS;
}


uint64_t
read () {
	uint64_t unin;
	return unin;
}

set.s

.type set, @function
.global set
set:
	movq $777, -16(%rsp)
	ret

Сборка и запуск
gcc  -Wall -Werror -Wno-uninitialized -O0 main.c set.s  && ./a.out
Выхлоп
777 was set
Объяснение

На тот момент когда вызвана функция set мы помещаем 777 на вершину стека + 1. Во время вызова read на вершину стека указывает неинициализированная переменная unin. Которая копируется через %rax в результат функции.

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

Тебе надо думать в обратном направлении - как из ассемблерной функции передать данные в вызывающий С код. А для этого тебе надо знать и следовать C ABI.

(удваиваю)

Вообще да. Передавай значение как нормальный пацан через x86 64 C ABI. Для одного значения это просто помещение в %rax перед возвратом

kostyarin_ ()
Последнее исправление: kostyarin_ (всего исправлений: 3)
Ответ на: комментарий от kostyarin_

помещаем 777 на вершину стека + 1

Точнее +2 (-16), т.к. Си ещё и базу сохраняет в стеке. Если передать GCC ещё и -fomit-frame-pointer, чтобы не следить за базой, то можно заменить -16 на -8.

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

Если я его правильно понял, то у него процедура ассемблерная в одном файле(file.s) и вней он создает стек, а сишная программа в которой он хочет извлечь данные из стека, в другом файле (c.cpp) потом он их видими вместе скомпилит и хочет получить данные из стека.

Основное дерьмо в том, что он использует ассемблерную вставку, вместо православного выноса ассемблерных функций в отдельный файл. Тогда встаёт вопрос зачем вообще выносить всё это дело куда-то, если есть вставка. А если это сторонняя функция, то зачем вставка. В общем парнишке нужно КМБ на ассемблере под Вендой пройти на бывшем wasm.ru нынешнем wasm.in.

Ассемблер тут непопулярен.

А парнишка, который мутил(ит?) LinASM тут не бывает?.. Или это я что-то путаю…

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

я отметил, как решенную тему. посути да, так и есть.

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

пожалуйста осветите. вот у нас есть адрес стека, он будет хранится в регистре ss. когда я создаю стек в программе на ассемблер, к примеру, то его адрес будет находится в ss, но когда я создам стек в ассемберной вставке, в с.cpp файле, там уже будет соовсе другой адрес стека , да и сам стек. вопрос, как взять адрес стека из кода на ассеблер, и передать этот адерс в ассемблерную вставку на С++ и после воспользоваться этим стеком уже в программе на С++. вот в чем проблема.

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

тут как раз тот случай, когда полезно будет рассказать, какая у тебя конечная цель.

в рамках стандартного c abi ты не «создаёшь стек»: твой поток исполнения запускается с уже установленным указателем стека, и предполагается, что весь твой код дальше действует согласно этому abi – и потому куски кода понимают друг друга.

в abi твоей платформы, в частности, будет указано, как передавать данные на стеке. делай, как там написано, и твой компилятор си обеспечит свою сторону кода.

естественно, ты можешь со стеком делать вещи, далеко выходящие за то, что разрешено abi, включая произвольную манипуляцию указателем на стек (см., например, swapcontext(3)).

но, опять же, для начала надо понять, что же ты хочешь сделать.

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

Ребята, где то я уже встречал такие темы. И че то ему нигде не помогают. Писали ему разную херь, но по делу практически ничего. Сам он умом небогат, думаю, что не обидется, и поэтому разобраться самосостояльно не может. И он , опять же по своей глупости пришел сюда, в надежде, что тут шарят и объяснят. Щас дня два помурыжат а после закинут эту тему. Он создаст ее снова и получит бан, с пометкой троллинг тупостью.

Хочет этот недалекий просто писать, на ассемблере, а писать понимая. Уперся видимо в стек.

Я знаю, потому что он на многих форумах уже просто за….бал всех. Кому это надо?

Если я шарил бы в ассемблере, то уже давно бы показал , как передать адрес стэка, и в новой программе работать с этим переданным стеком. Но я не знаю как. А давать ему глупые советы не хочу. Просто он заблудится окончательно.

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

Если я шарил бы в ассемблере, то уже давно бы показал , как передать адрес стэка, и в новой программе работать с этим переданным стеком. Но я не знаю как. А давать ему глупые советы не хочу. Просто он заблудится окончательно.

Да сиди. Если ты программишь уже эн-времени, то всё понятно. А когда ты малой, и учишься вот-таким вот способом, то ничего не понятно. Я про это долбанный стек и регистры 100 разных книжек прочитал. И если с регистрами всё более менее, то когда читаешь про память, то каждая ссаная книженция начинает свой заунывный вой про начальный регион адреса BIOS, 48 килобайт, и всякое прочее дерьмо, которое в принципе никому и нигде не нужно. Но по незнанке я ещё пытался всё это понять и структурировать. А как на меня снизошло просветление я уже не помню. Но это было тяжело идолго. В общем и целом нужно трогать не только ассемблер, но и линкер и Си и шарды писать и линоквать по всякому. Чтобы всё это дело понять. Т.к. там всёсвязано и одно без другого никак не завершает картину.

Проблема в том – понять уровень парнишки, и чего он реально хочет.

kostyarin_ ()

Вообще, если брать общий случай, то программа под Linux выглядит примерно таким вот образом.

[куча]
   |
   V


   ^
   |
[стек]

Типа куча растёт вниз, от начального адреса (который вовсе не нулевой, но пока этим не заморачивайся). А стек растёт снизу вверх. И в куче само тело программы, которое исполняется. А между ними неразмеченная память, которую нельзя использовать.

При запуске программы Linux кладёт в стек переменные окружения и аргументы. Потом передаёт управление точке входа программы. Для программ на Си в точке входа выполняется кое-какая работа, чтобы функция main выглядела именно так как выглядит. И чтобы её результат был кодом завершения.

Программа состоит из функций. Для обычного случая, допусти, если нет оптимизаций и все функции публичные (т.е. не static и не inline и прочее) вызов функции в Linux следует договорённости fast-call. Т.е. частично используются регистры - -частично стек. Для простого случая, когда функция не имеет аргументов и ничего не возвращает – вызов функции проходит так:

  1. выровнять стек по 16-ти-байтной границе
  2. вытолкнуть в стек адрес следующей инструкции
  3. передать управление вызываемой функции

вызываемая функция при этом ещё и сохраняет базу в стеке, и возвращает её потом обратно

push  %rbp
movq %rsp, %rbp

; код самой функции

pop    %rbp
movq %rbp, %rsp
ret

Если gcc передать -fomit-frame-pointer, то базу сохранять она не будет. Но это пока опустим. Будет плясать от обычного случая. Т.е. везде при вызове функции используется стек. Как и в самой функции используется стек для локальных переменных. Например



int
do_domething () {
    uint64_t value;
    uint64_t another;


    // тело функции

    return 0;
}

У этой функции есть две локальные переменные. Когда функция вызвана, то их место – это место на вершине стека. Т.е., допустим есть вызов этой функции из main


int
main () {

    do_something (); // <---- (1)

    return EXIT_FAILURE; // <--- (2)
}

В момент вызова (1) происходит следующее:

  1. стек выравнивается, но он и так выровнен, так что пропускаем
  2. в стек помещается текущий адрес следующей за вызовом инструкции (следующая инструкция return (2))
  3. передаётся управление функции do_something
  4. do_domething помещает базу в стек
  5. do_domething использует стек для своих двух локальных переменных

(стек растёт снизу вверх)


[ место для uint64_t another;       ]  } subq $16, %rsp
[ место для uint64_t value;         ]  }
[база вытолкнутая сюда в do_domthing]   push %rbp
[адрес возврата (2)                 ]   call do_something
[стек до вызова do_domething        ]   

Таким образом, если do_something вызовет ещё какую-то функцию, то стек будет наращиваться сверху этого, и переменные сохраняться.

При возврате из do_domething стек разматывается обратно. Вот и всё. При этом то что было «записано» в стек – там и остаётся. Например, есть две функции, которые вызываются одна за другой


// gcc  -Wall -Werror -Wno-uninitialized -O0 read_stack_left.c && ./a.out 


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

void write ();
void read ();

int
main() {
	write ();
	read ();
	return EXIT_SUCCESS;
}

void
write () {
	uint64_t value = 50; // <--- будет лежать на стеке в своём месте

	printf ("%ld was written\n", value);
}

void
read () {
	uint64_t value; // <--- возьмёт то что там лежало

	printf ("%ld was read\n", value);
}

Результатом которых будет

50 was written
50 was read

Т.е. первая функция пишет в переменную (место на стеке) 50, потом она возвращается и стек разматывается обратно. Во время вызова второй функции на месте уже ёё переменной уже записано 50.

Вот и всё. Это на самом деле просто, если сильно не углубляться в ненужные дебри.

kostyarin_ ()
Последнее исправление: kostyarin_ (всего исправлений: 2)
Ответ на: комментарий от kostyarin_

Скажи пожалуйста, а что лежит в rbp изначально? Щас секунду... каждая подпрограмма, это та которая, вызывается другой программой, в нашем случае, это любая фукция, которая будет вызвана в другой фукции, так вот каждая подпрограмма создает стековый фрейм, она положит на стек свои формальные и фактические параметры. Как будет исполнятся программа, ядро пнет фукцию start(), которая пнет фукцию main()значит на стеке будет как минимум два стековых фрейма. Ebp это регистр для произвольного доступа к стеку, но почему тогда автору тему советуют использовать rsp вместо rbp? Я думаю что всем будет интересно знать что будет лежать в rbp

Push %rbp
mov %rsp, %rbp

pop %rsp
mov %rbp, %rsp
Это положит в rsp функция start()?
А после main()?

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

Не формальные, а локальные. На стек при вызове подпрограммы ложатся ее фактические параметры, и локальные переменные. Например

[code]

foo(3, 5);//ФАКТИЧЕСКИЕ ПАРАМЕТРЫ. Первыми на стэк лягут они. Ну так заявляют. А потом локальные переменные.

Ну это foo(){int a;} "а"тут локальная переменная. Она также разместится на стэке.

[/code]

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

Цитирую

push %rbp
mov %rsp, %rbp

sub 64, %rbp

mov $777, -24(%rbp)

Конец цитаты.

но почему тогда автору тему советуют использовать rsp вместо rbp

В цитате выше, ТС манипулирует %rbp, чтобы «выделить» стек, что вводит в заблуждение.

%rbp – база, %rsp – вершина. Т.е. по сути %rbp указывает на то положение стека, которое было на момент вызова функции. Граница, ниже которой находятся стековые кадры предыдущих функций, аргументы и возвращаемое значение, в некоторых случаях и т.д.

Ebp это регистр для произвольного доступа к стеку

Для примера

void
some () {
    uint64_t v1;
    uint64_t v2;

    // stuff

    one_another ();
}

Функция, условно, может и не следить за базой. Но по умолчанию GCC добавляет это.

В момент входа в функцию, она, сама вызываемая функция, сохраняет %rbp в стеке. Там скорее всего база предыдущей функции. Но нас это не волнует, т.к. согласно fast-call, вызываемая функция сохраняет %rbp, если собирается использовать (callee saved registers).

В общем, она его сохраняет

push %rbp

и помещает туда указатель на текущий стековый кадр, т.е. на базу текущей функции

mov %rsp, %rbp

Дальше на стеке идут локальные переменные. И функции some, для того, чтобы вызвать one_another и при этом сохранить свои локальные переменные нужно двигать %rsp.

subq $16, %rsp ; // uint64_t v1, v2;

В противном случае вызов one_another заталкивая в стек всякое – перезапишет их.

В итоге выходит, что к локальным переменным мы можем обращаться используя (1) %rip, (2) %rsp, (3) %rbp.

Оба, %rip и %rsp, по ходу функции могут меняться и ориентироваться на них трудно. Тогда как %rbp и есть начало локальных переменных. Т.е.

movq $1, -0(%rbp) ; // v1 = 1
movq $2, -8(%rbp) ; // v2 = 2

Именно это и имеется ввиду, когда говорится

Ebp это регистр для произвольного доступа к стеку

Т.к. %rsp двигается от call и ret инструкций, и нам нужно его двигать, чтобы сохранить локальные переменные. А %rbp – в рамках вызова – константа.


И я не уверен, что что-то до main использует %rbp для своего фрейма. Ну да это и не особо важно.

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

На стек при вызове подпрограммы ложатся ее фактические параметры, и локальные переменные.

В конвенции вызовов fast-call широко используются регистры. Т.е. пара целочисленных аргументов будет передана через регистры и стек это не затронет. Если передавать и возвращать что-то большое, то будет использован и стек. Причём это забота вызывающей функции. И всё это будет за пределами вызванной, как бы перед ней на стеке.

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

Это сильно!!!

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


push %rbp// положу на стек, то что было в регистре rbp, чтобы на затереть

mov %rsp, %rbp // положу теперь в rbp адрес вершины стека.

sub $28 , %rsp //  выделю место данных, которые захочу разместить.  это цифра, должна быть кратной двум. минимально что может принять в себя стек, это два байта. то есть слово.  могу тут врать.


и делал так, 

mov $777, -24(%rsp)
mov $888, -16(%rsp)
mov $555,  -8(%rsp)

 


после я восстанавливал

pop %rbp
mov %rbp, %rsp

то есть, если я вдруг до момента 

mov %rbp, %rsp

решил бы создать новый стековый кадр, то  делая 
pop %rbp
mov %rsp, %rbp //  я бы не затер никакие данные и не потерял. 

мне так казалось

спасибо огромное , что находите время.  я пытаюсь показать как я мыслил, чтобы было лучше понятно, что именно есть причина моего непонимания.



но гланое это то, что для меня еще открыт вопрос как сделать



file.s

.global foo
foo:

push $999


ret



fil.cpp

#include <iostream>

int main()
{

int64_t a=0; 

asm
(
"call foo\n"
// теперь нужно взять как-то адрес стека из регистра ss  и достать из него 999 после положить его в регистра rax. 

все это нужно сделать именно с помощь адреса того стека, который был создан в процедуре foo.


"mov %%rax, %1\n"
"mov %0, %%rax\n"


:"=r"(a)
:"r"()


);

std::cout << a;
return 0;
}

gcc file.s -c
gcc fil.cpp -masm=att -no-pie -c

gcc fil.o file.o -o start

если можете то научите

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

теперь нужно взять как-то адрес стека из регистра ss

%ss – это сегментный регистр, к стеку отношение он имеет весьма посредственное на платформе x64, и, если не ошибаюсь, то Linux его использует в своих целях. Речь про %rsp, ведь так?

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

Я понял что он хочет….

mov $999, %rax ; так он может за раз передать только одно какое либо значение а он хочет передать сразу много. Например массив, который будет хранится на стеке. Теперь он хочет взять этот стек и работать с ним в другой программе. Ему нужен стек. Создаст его в коде на ассемблер, а будет работать с ним в Си, к примеру.

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

Корректива, с учётом сказанного @LGH.

oa.c

// gcc  -Wall -Werror -O0 oa.c oa.s  && ./a.out 


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

void     set ();

int
main() {

	// reference point point, we will not use it explicitly,
	// we will use its address to access stack frame of the
	// 'set' function
	uint64_t val = 0;

	// set value on the stack
	set ();

	// explanation
	//
	// 1. before the set called
	//
	//         [ val                      ] <-- %rsp
	//    ---> [ main base                ]
	//
	// 2. set calling
	//
	//         [ next instruction address ] <-- %rsp
	//         [ val                      ]
	//    ---> [ main base                ]
	//
	// 3. inside the set
	//
	//         : $333                     :
	//         : $222                     :
	//         : $111                     :
	//         [ next instruction address ] <-- %rsp (we don't touch it,
	//         [ val                      ]           we don't use %rbp)
	//    ---> [ main base                ]
	//
	// 4. returning form the set
	//
	//         : $333                     :
	//         : $222                     :
	//         : $111                     :
	//         : next instruction address :
	//         [ val                      ] <-- %rsp
	//    ---> [ main base                ]

	// so, using address of the val, we can access the $111, $222 and $333;
	// size of the 'next instruction address' is 8 byte

	// since, the val is type of uint64_t it's size is 8 byte too

	printf ("%ld, %ld and %ld were set\n",
		 *(&val - 2), *(&val - 3), *(&val - 4));

	// note: we can't use successive calls of the printf, since the call
	// overwrites stack

	return EXIT_SUCCESS;
}

oa.s

.type set, @function
.global set
set:
	# don't toucn %rbp and %rsp at all; the %rsp points to the base
	# of this 'set' function; let's use it as the base

	movq $111,  -8(%rsp) # push $111
	movq $222, -16(%rsp) # push $222
	movq $333, -24(%rsp) # push $333

	# we didn't modify the %rsp, we don't have to restore it

	ret

Компиляция, сборка и запуск
gcc  -Wall -Werror -O0 oa.c oa.s  && ./a.out 
Объяснение

Использую адрес локальной переменной как отправную точку и всё.

kostyarin_ ()
Ответ на: удаленный комментарий

заталкивая значение в файле f.s на стек, будет создан один стэк, а в файле с.сpp где он использует ассемблерные вставки при вызове фукции main будет использован другой стек. Почему так?

На всю программу операционной системой изначально выделяется только один стек, не зависимо от того, из скольки исходных файлов она состоит. Стек не создаётся в программе.

То есть «стек файла f.s» и «стек файла c.cpp» — это один и тот же стек.

Вы с автором, случайно, не братья?

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

Я его знаю очень хорошо… учились вместе. Не общались только. Он в контакте ссылку на этот форум кинул в теме Линукса, ну я и многие еще и пришли сюда.

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

А скажите, что происходит со стеком после как программа отработала? Из него можно извлечь данные соовсем в другой программе? То есть c.cpp. и s.cpp два файла, которые будут скомпилированны отдельно друг от друга

    


g++ с.сpp //тут был стек
g++ s.cpp// можно ли от сюда к нему обратиться?



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

после как программа отработала? Из него можно извлечь данные соовсем в другой программе?

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

anonymous ()