LINUX.ORG.RU

NASM. Передача параметров через стек. Ничего не выходит(


0

0

Здравствуйте, я вообщем тут изучаю ассемблер (nasm), в процессе обучения наткнулся на такую ошибку что не могу получить аргументы при вызове своей программы.

Вот текст (test.asm):
; section .data
; s : db "test"

section .text
global _start

_start:
pop rcx ; число аргументов
pop rcx ; имя программы
pop rcx ; первый параметр

; mov rcx, s

mov rax, 4 ; sys_write
mov rbx, 1 ; stdout
; в rcx содержится первый входной параметр
mov rdx, 4 ; длина строки, ставлю например 4
int 80h

mov rax, 1 ; sys_exit
mov rbx, 0 ; код завершения
int 80h

Компилирую так:
$ nasm -f elf64 test.asm
$ ld -s -o test test.o

Запускаю:
$ ./test one two three four five six seven
$

- Результат как видите пусто. Во всех туториалах (ну например http://leto.net/writing/nasm.php) описывается что аргументы программы можно получить через стек, но вот у меня ничего не выходит. Что я делаю не так?

ЗЫ:
$ uname -a
Linux debian.site 2.6.22-3-amd64 #1 SMP Sun Nov 4 18:18:09 UTC 2007 x86_64 GNU/Linux

x86 и x86_64 ABI различаются. В частности передаваемые параметры в С функцию передаются через регистры а не стек. Напишите простенькую программку на C и оттрансливуйте в asm (gcc -S).

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

расстолкуйте пожалуйста:
void main(int argc, char** argv)
{
char* b = argv[0];
}

<..куча мусора..>
.globl main
.type main, @function
main:
.LFB2:
pushq %rbp
.LCFI0:
movq %rsp, %rbp
.LCFI1:
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movq -32(%rbp), %rax
movq (%rax), %rax
movq %rax, -8(%rbp)
leave
ret
<..куча мусора..>

А то мне как новичку с адресацией пока тяжело да тем более с другого ассемблера. Буду очень признателен за маленький экзампл :)

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

ассемблер тут и в NASM один и тот же, x86_64. Синтаксис разный -- у NASM Intel синтаксис, gcc -S = AT&T. В AT&T синтаксисе всегда пишется размер операндов (movl,movq,movw, movb, регистры с %, константы с $, операнды слева исходный направо назначение -- то есть, mov ax,"0001" = strcpy("0001",ax) = movw $"0001",%ax). Более похоже на ассемблер Motorola 68000. Подробнее AT&T синтаксис описан в info gas.

В целом этот пример не показателен, возьми что-то с printf/write хотя бы, с 2 функциями, посмотреть во что разворачивается вызов локальной функции и сисколла.  Хотя и так понятно: void main(int argc, char** argv) : пусть RSP-вершина стека при входе в main, cdecl соглашение вызова, первый параметр на вершине стека, по большему адресу, последний на дно, по меньшим адресам.  Тогда 
RSP+sizeof(char**)=argv, RSP+sizeof(char**)+sizeof(int)=argc. Локальная переменная в стеке, по адресу RSP-sizeof(char*)=b.
sizeof(rbp)=8 байт, получается стек

AT&T                   ; INTEL                ; СТЕК

main:                                         ;  argc,argv,mainsp,(b)
.LFB2: 
pushq %rbp             ; push rbp                    ; old_bp,mainsp,argv,argc  
.LCFI0: 
movq %rsp, %rbp        ; mov rbp, rsp         ; argc,argv,mainsp,old_rbp,0,b,?1,argc,argv -- создали стековый фрейм, rbp=адрес 0
.LCFI1: 
movl %edi, -20(%rbp)   ; mov dword ptr [rbp-4*5],edi    ;??argc?(из сискола execl)
movq %rsi, -32(%rbp)   ; mov qword ptr [rbp-4*8],rsi    ;??argv
movq -32(%rbp), %rax   ; mov rax,qword ptr [rbp-4*8]    ;argv
movq (%rax), %rax      ; mov rax,qword ptr [rax]        ;(*argv)
movq %rax, -8(%rbp)    ; mov qword ptr [rbp-8],rax      ;b=argv[0]=(*argv)
leave                  ; leave
ret                    ; ret

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

ассемблер тут и в NASM один и тот же, x86_64. Синтаксис разный -- у NASM Intel синтаксис, gcc -S = AT&T. В AT&T синтаксисе всегда пишется размер операндов (movl,movq,movw, movb, регистры с %, константы с $, операнды слева исходный направо назначение -- то есть, mov ax,"0001" = strcpy("0001",ax) = movw $"0001",%ax). Более похоже на ассемблер Motorola 68000. Подробнее AT&T синтаксис описан в info gas.

В целом этот пример не показателен, возьми что-то с printf/write хотя бы, с 2 функциями, посмотреть во что разворачивается вызов локальной функции и сисколла.  Хотя и так понятно: void main(int argc, char** argv) : пусть RSP-вершина стека при входе в main, cdecl соглашение вызова, первый параметр на вершине стека, по большему адресу, последний на дно, по меньшим адресам.  Тогда 
RSP+sizeof(char**)=argv, RSP+sizeof(char**)+sizeof(int)=argc. Локальная переменная в стеке, по адресу RSP-sizeof(char*)=b.
sizeof(rbp)=8 байт, получается стек

AT&T                   ; INTEL                ; СТЕК

main:                                         ;  argc,argv,mainsp,(b)
.LFB2: 
pushq %rbp             ; push rbp                    ; old_bp,mainsp,argv,argc  
.LCFI0: 
movq %rsp, %rbp        ; mov rbp, rsp         ; argc,argv,mainsp,old_rbp,0,b,?1,argc,argv -- создали стековый фрейм, rbp=адрес 0
.LCFI1: 
movl %edi, -20(%rbp)   ; mov dword ptr [rbp-4*5],edi    ;??argc?(из сискола execl)
movq %rsi, -32(%rbp)   ; mov qword ptr [rbp-4*8],rsi    ;??argv
movq -32(%rbp), %rax   ; mov rax,qword ptr [rbp-4*8]    ;argv
movq (%rax), %rax      ; mov rax,qword ptr [rax]        ;(*argv)
movq %rax, -8(%rbp)    ; mov qword ptr [rbp-8],rax      ;b=argv[0]=(*argv)
leave                  ; leave
ret                    ; ret

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

Вот код на Си:
#include <stdio.h>
void main(int argc, char** argv)
{
printf(argv[0]);
}

Вот это оттранслированный код:
.file "main.c"
.text
.globl main
.type main, @function
main:
.LFB2:
pushq %rbp
.LCFI0:
movq %rsp, %rbp
.LCFI1:
subq $16, %rsp
.LCFI2:
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movq -16(%rbp), %rax
movq (%rax), %rdi
movl $0, %eax
call printf
leave
ret
.LFE2:
.size main, .-main
.section .eh_frame,"a",@progbits
.Lframe1:
.long .LECIE1-.LSCIE1
.LSCIE1:
.long 0x0
.byte 0x1
.string "zR"
.uleb128 0x1
.sleb128 -8
.byte 0x10
.uleb128 0x1
.byte 0x3
.byte 0xc
.uleb128 0x7
.uleb128 0x8
.byte 0x90
.uleb128 0x1
.align 8
.LECIE1:
.LSFDE1:
.long .LEFDE1-.LASFDE1
.LASFDE1:
.long .LASFDE1-.Lframe1
.long .LFB2
.long .LFE2-.LFB2
.uleb128 0x0
.byte 0x4
.long .LCFI0-.LFB2
.byte 0xe
.uleb128 0x10
.byte 0x86
.uleb128 0x2
.byte 0x4
.long .LCFI1-.LCFI0
.byte 0xd
.uleb128 0x6
.align 8
.LEFDE1:
.ident "GCC: (GNU) 4.2.3 20071123 (prerelease) (Debian 4.2.2-4)"
.section .note.GNU-stack,"",@progbits

Здесь всё работает. Но если скомпилить и запустить вот этот код (без использования libc), то он работать не будет:
.text
.globl _start
_start:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movq -16(%rbp), %rax
// выполнение следующей строки приводит к сегфолту:
movq (%rax), %rdi
// sys_write
mov $4, %rax
mov $1, %rbx
// ...здесь в rcx мы должны положить первый аргумент...
mov $10, %rdx
int $0x80
// sys_exit
mov $1, %rax
mov $0, %rbx
int $0x80

Их этого можно посудить что получение аргументов таким способом применительно только к gas+glibc? Не так ли?

А как получить аргументы моей программе на ассемблере без использования glibc? %rdi, %rsi, ... всё по перепробовал, сегфолтится зараза :(

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

>А как получить аргументы моей программе на ассемблере без использования glibc? %rdi, %rsi, ... всё по перепробовал, сегфолтится зараза :(

Причем тут аргументы, у вас syscall write() не работает.

Посмотрите файл /usr/src/linux/include/asm-x86_64/unistd.h, там указаны номера сисколов для x86-64, write это "1". В этом же файле указаны макросы _syscall0(), _syscall1() и т.д.

Вам уже сказали, что параметры передаются в ядро через другие регистры, чем на x86, но если не верите, то можно написать программу на Си, которая будет делать syscall без обращения к glibc и посмотреть ее ассемблерный листинг.

extern int errno;
#include <asm-x86_64/unistd.h>

_syscall3(int, write, uint, fd, char *, buf, uint, count);
_syscall1(int, exit, int, code);

int main()
{
write(1, "test", 4);
return 0;
}

void _start()
{
main();
exit(0);
}


компилить:
gcc -O2 -I/usr/src/linux/include -o test.o -c test.c
ld -s -o test test.o

А относительно получения параметров, так вроде они просто лежат в стеке, без указания их кол-ва. И _start() их просматривает, считает их (argc++) пока не будет встречен NULL, и вызывает main().

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

сегфолтится потому что http://lkml.org/lkml/2006/6/6/11

в x86_64 другое ABI сисколлов, см. из примера по ссылкам, что я постил выше
по второй ссылке как раз execl/argc. Еще есть пример argc/argv для x86_32, 2.7 в http://www.openvms-rocks.com/~hophet/docs/0x01_asm-avancado.txt. Для 64bit что-то аналогично. Попробуй void main(int argc, char** argv) { for(;argc>0;){puts(argv[argc--]}; argc=0x123; argv[0]="";} и посмотреть в районе argc=.. argv=..

#================================
# WRITE_STDOUT
#================================
# ecx has string
# eax,ebx,ecx,edx trashed


write_stdout:
push %rdx
push $SYSCALL_WRITE # put 4 in eax (write syscall)
pop %rax # in 3 bytes of code

cdq # clear edx

lea 1(%rdx),%edi # put 1 in ebx (stdout)
# in 3 bytes of code

mov %ecx,%esi

str_loop1:
inc %edx
cmpb $0,(%rcx,%rdx) # repeat till zero
jne str_loop1

syscall # run the syscall
pop %rdx
ret

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

не, не поэтому сегфолтится. А потому что  
                                                         ; x=rsp, *rsp =argc argv 
pushq %rbp                                    ;               *rsp=argc argv  old_bp
movq %rsp, %rbp                           ; bp=rsp, *
subq $16, %rsp                             ;rsp-=16 (2*8, место для локальных переменных)
movl %edi, -4(%rbp)                     ; первый параметр =argc
movq %rsi, -16(%rbp)                  ; второй=argv
movq -16(%rbp), %rax                 ; rax = argv
// выполнение следующей строки приводит к сегфолту: 
movq (%rax), %rdi                        ; rdi = *argv = (*NULL) если ты не указывал аргументы в ком. строке                                           ;первый параметр=адрес строки в write

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

>А как получить аргументы моей программе на ассемблере без использования glibc?

чтобы было понятнее, покури исходники сисколла execve/execl и кусок RTL glibc в районе startup-кода, который вызывает _main. Ты запускаешь ./myprog 1 2 3, шелл делает exec ("./myprog","./myprog", "1", "2", "3"), с параметрами в rdi, rsi, и т.п. см. ABI сисколлов, который грузит ELF-загрузчиком новый процесс, создаёт и настраивает таблицу процесса, запускает процесс по адресу в заголовке ELF. По адресу в заголовке торчит кусок из RTL, который подготавливает хип, выравнивает на границу параграфа хип/стек/итп, и в конечном итоге вызывает main. Аргументы main берутся из стека, или если ещё ничего не успело их испортить, прямо из rdi, rsi и т.п., установленных при вызове сисколла exec. После return из main прибивается хип, в конце вызывается сисколл exit.

В общем, покрути исходники RTL glibc до вызова _main, чтобы было понятнее

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

> Причем тут аргументы, у вас syscall write() не работает.
Неправда. Смотрите:
$cat main.s
.text
.globl _start
_start:
// sys_write:
// 4 - syswrite
mov $4, %rax
// 1 - stdout
mov $1, %rbx
// s - строка
mov $s, %rcx
// 14 - длина строки
mov $14, %rdx
// вызываю
int $0x80
// sys_exit
mov $1, %rax
mov $0, %rbx
int $0x80
s:
.string "Hello, World!\n\0"
$ as -o main.o main.s
$ ld -s -o main main.o
$ ./main
Hello, World!
$

Как видите, работает :) Также пробовал syscall_open, syscall_exit тож работает.
/usr/src/linux/include/asm-x86_64/unistd.h смотрел, там действительно другие номера вызовов, не такие как в /usr/src/linux/include/asm-i386. Но если поставить "1" вместо "4" то программа ничего не выводит. Кстати я где-то на x86-64.org читал там вместо "int $0x80" используют "syscall" но так и не понял - зачем это...

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

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

> rdi = *argv = (*NULL) если ты не указывал аргументы в ком. строке

указывал, чесс. слово
$ ./main a b c d
Ошибка сегментирования
$

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

>почему-то Компиляция вашего примера вылетает с большим числом ошибок

Да, туда надо бы добавить #include <linux/types.h> и убрать extern... У вас какой бинарник получается 64 или 32 бит? То есть что говорит команда "file бинарный_файл".

Инструкция syscall на x86-64 выполняется быстрее, чем int 0x80. Я тут прочитал, что вроде если использовать int $0x80, то номера сисколов будут как на i386, а если syscall, то будут номера будут как на x86-64.

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

> Да, туда надо бы добавить #include <linux/types.h> и убрать extern...

Не... все равно ругается, всё какую-то мать вспоминает...
#include <linux/unistd.h>
#include <sys/syscall.h>
#include <linux/linkage.h>
#include <linux/types.h>
_syscall3(int, write, uint, fd, char *, buf, uint, count);
_syscall1(int, exit, int, code);
int main()
{
write(1, "test", 4);
return 0;
}
void _start()
{
main();
exit(0);
}

$ gcc -O2 -I/usr/src/linux/include -o test.o -c test.c
test.c:6: error: expected declaration specifiers or ‘...’ before ‘write’
test.c:6: error: expected declaration specifiers or ‘...’ before ‘fd’
test.c:6: error: expected declaration specifiers or ‘...’ before ‘buf’
test.c:6: error: expected declaration specifiers or ‘...’ before ‘count’
test.c:6: warning: data definition has no type or storage class
test.c:7: error: expected declaration specifiers or ‘...’ before ‘exit’
test.c:7: error: expected declaration specifiers or ‘...’ before ‘code’
test.c:7: warning: data definition has no type or storage class
test.c: In function ‘_start’:
test.c:18: warning: incompatible implicit declaration of built-in function ‘exit’

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

Самый верный способ узнать почему не работает - запустить в отладчике. Там и содержимое стека можна увидеть. Пытаться изучать assembler без отладчика - мягко говоря неправильно :)

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