Передача структур между ассемблером и C/C++

Последнее обновление: 25.01.2024

Структуры представляют удобную организацию данных и нередко применяются в качестве параметров или результата функций на C/C++. Рассмотрим на примере Linux, как мы можем передать структуры из кода Си в код на ассемблере и наоборот из ассемблера в функцию на С/С++.

Получение в ассемблере структур из функции на Си

Пусть у нас есть файл person.c со следующим кодом:

struct person{char* name; int age;};

struct person create_person()
{
    struct person tom = {.name = "Tom", .age=39};
    return tom;
}

Здесь определен структурный тип person с двумя полями - name и age (условно имя и возраст пользователя). Функция create_person возвращает одну структуру. Скомпилируем этот файл в объектный:

gcc -c person.c -o person.o

Далее дизассемблируем его:

root@Eugene:~/asm# objdump -d -M intel person.o

person.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <create_person>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   rbp
   5:   48 89 e5                mov    rbp,rsp
   8:   48 8d 05 00 00 00 00    lea    rax,[rip+0x0]        # f <create_person+0xf>
   f:   48 89 45 f0             mov    QWORD PTR [rbp-0x10],rax
  13:   c7 45 f8 27 00 00 00    mov    DWORD PTR [rbp-0x8],0x27
  1a:   48 8b 45 f0             mov    rax,QWORD PTR [rbp-0x10]
  1e:   48 8b 55 f8             mov    rdx,QWORD PTR [rbp-0x8]
  22:   5d                      pop    rbp
  23:   c3                      ret
root@Eugene:~/asm#

Здесь мы видим, что функция create_person возвращает данные структуры через регистры RAX (поле name) и RDX (поле age). В частности, строка, которая передается в поле name сначала загружается в RAX, а затем в стек по адресу [rbp-0x10]:

lea    rax,[rip+0x0]
mov    QWORD PTR [rbp-0x10],rax

Затем name помещается обратно в RAX:

mov    rax,QWORD PTR [rbp-0x10]

Аналогично данные для поля age - число 39 (0x27) сначала помещается в стек по адресу [rbp-0x8], а затем помещается в регистр RDX:

mov    DWORD PTR [rbp-0x8],0x27
..................................
mov    rdx,QWORD PTR [rbp-0x8]

Таким образом, здесь мы видим классическую схему, когда из функции возвращаются два значения, поэтому для их передачи согласно ABI применяются два регистра - RAX и RDX. Благодаря этому не составит труда получить эти данные в коде ассемблера. Допустим, файл будет называться app.asm и будет иметь следуюший код:

global main
 
extern printf ; подключаем функцию printf
extern create_person ; подключаем функцию create_person
 
section .data
name db "Name: %s",10, 0   ; строка для вывода поля name
age db "Age: %d",10, 0   ; строка для вывода поля age
 
section .text
main:
    sub rsp, 24         ; 16 байт для переменных + 8 байт выравниваение

    call create_person  ; функция возвращает структуру person в RAX(name):RDX(age)

    mov [rsp+16], rax   ; сохраняем name в стек
    mov [rsp+8], rdx    ; сохраняем age в стек

    mov rdi, name       ; выводим name
    mov rsi, [rsp+16]
    call printf

    mov rdi, age       ; выводим age
    mov rsi, [rsp+8]
    call printf

    add rsp, 24              ; восстанавливаем стек
    ret

В данном случае сохраняем полученные после вызова create_person данные в стек и затем извлекаем их оттуда для вывода на консоль с помощью функции printf. Пример компиляции и работы программы:

root@Eugene:~/asm# nasm -f elf64 app.asm -o app.o
root@Eugene:~/asm# gcc person.c app.o -static -o app
root@Eugene:~/asm# ./app
Name: Tom
Age: 39
root@Eugene:~/asm#

Сложная структура

Если структура содержит больше двух простых полей, то ее содержимое возвращается из функции через стек. Например, изменим структуру person в файле person.c следующим образом:

struct person{int id; char* name; int age;};

struct person create_person()
{
    struct person tom = {.id=112, .name = "Tom", .age=39};
    return tom;
}

Теперь структура содержит три поля. Если мы скомпилируем объектный файл и дизассемблируем его, то мы увидим ассемблерный код типа следующего:

root@Eugene:~/asm# gcc -c person.c -o person.o
root@Eugene:~/asm# objdump -d -M intel person.o

person.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <create_person>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   rbp
   5:   48 89 e5                mov    rbp,rsp
   8:   48 89 7d d8             mov    QWORD PTR [rbp-0x28],rdi   
   c:   c7 45 e0 70 00 00 00    mov    DWORD PTR [rbp-0x20],0x70
  13:   48 8d 05 00 00 00 00    lea    rax,[rip+0x0]        # 1a <create_person+0x1a>
  1a:   48 89 45 e8             mov    QWORD PTR [rbp-0x18],rax
  1e:   c7 45 f0 27 00 00 00    mov    DWORD PTR [rbp-0x10],0x27
  25:   48 8b 4d d8             mov    rcx,QWORD PTR [rbp-0x28]
  29:   48 8b 45 e0             mov    rax,QWORD PTR [rbp-0x20]
  2d:   48 8b 55 e8             mov    rdx,QWORD PTR [rbp-0x18]
  31:   48 89 01                mov    QWORD PTR [rcx],rax
  34:   48 89 51 08             mov    QWORD PTR [rcx+0x8],rdx
  38:   48 8b 45 f0             mov    rax,QWORD PTR [rbp-0x10]
  3c:   48 89 41 10             mov    QWORD PTR [rcx+0x10],rax
  40:   48 8b 45 d8             mov    rax,QWORD PTR [rbp-0x28]
  44:   5d                      pop    rbp
  45:   c3                      ret
root@Eugene:~/asm#

Вначале значения сохраняются в стек

mov    QWORD PTR [rbp-0x28],rdi   ; значение параметра-указатель помещаем в стек по адресу [rbp-40]
mov    DWORD PTR [rbp-0x20],0x70  ; сохраняем id (112) по адресу [rbp-32]
lea    rax,[rip+0x0]              ; загружаем строку name в RAX
mov    QWORD PTR [rbp-0x18],rax   ; сохраняем указатель name по адресу [rbp-24]
mov    DWORD PTR [rbp-0x10],0x27  ; сохраняем age(39) по адресу [rbp-16]

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

mov    rcx,QWORD PTR [rbp-0x28]     ; в регистр RCX получаем указатель
mov    rax,QWORD PTR [rbp-0x20]     ; в регистр RAX id
mov    rdx,QWORD PTR [rbp-0x18]     ; в регистр RDX указатель на name
mov    QWORD PTR [rcx],rax          ; по адресу из регистра RCX (в указатель) копируем из регистра RAX значение id
mov    QWORD PTR [rcx+0x8],rdx      ; к адресу из указателя в RCX прибавляем 8 и сохраняем по нему из RDX указатель на name
mov    rax,QWORD PTR [rbp-0x10]     ; в RAX копируем значение age
mov    QWORD PTR [rcx+0x10],rax     ; к адресу из указателя в RCX прибавляем 16 и сохраняем по нему из RAX указатель на age
mov    rax,QWORD PTR [rbp-0x28]     ; в RAX сохраняем указатель, который хранится в [rbp-40]

Если отвлечься от конкретной функции, то через регистр RDI передается указатель на область памяти, в которой будет сохраняться данные структуры. Далее в этой области данных помещаются значения полей структуры в том порядке, в котором они определены в самой структуре.

Для получения структуры определим файл app.asm со следующим кодом:

global main
 
extern printf ; подключаем функцию printf
extern create_person ; подключаем функцию create_person
 
section .data
name db "Name: %s",10, 0   ; строка для вывода поля name
age db "Age: %d",10, 0   ; строка для вывода поля age
id db "Id: %d",10, 0   ; строка для вывода поля id
 
section .text
main:
    sub rsp, 24           ; 24 байта для переменных

    mov rdi, rsp        ; передаем адрес, по которому будет сохраняться возвращаемая структура
    call create_person  ; функция возвращает структуру person, указатель в RAX

    mov rdi, id       ; выводим id
    mov rsi, [rsp]
    call printf

    mov rdi, name       ; выводим name
    mov rsi, [rsp+8]
    call printf

    mov rdi, age       ; выводим age
    mov rsi, [rsp+16]
    call printf

    add rsp, 24              ; восстанавливаем стек
    ret

Прежде всего выделяем в стеке 24 байта для данных структуры. Хотя два поля структуры представляют тип int (4 байта), но для каждого поля выделяется по 8 байт.

Затем в регистр RDI передается адрес области в стеке. В итоге после вызова функции create_person мы получим стек

idrsp
namersp+8
agersp+16

Соответственно, используя адреса в стеке, мы можем получить значения структуры и вывести их с помощью функции printf. Пример компиляции и работы программы:

root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o
root@Eugene:~/asm# gcc person.c  hello.o -static -o hello
root@Eugene:~/asm# ./hello
Id: 112
Name: Tom
Age: 39
root@Eugene:~/asm#

Передача структуры из ассемблера в код на Си

Для передачи структуры из ассемблера в код Си в качестве параметра применяется стек. Например, пусть у нас есть файл person.c со следующим кодом:

#include <stdio.h>

struct person{int id; char* name; int age;};

void print_person(struct person bob)
{
    printf("Id: %d \n", bob.id);
    printf("Name: %s \n", bob.name);
    printf("Age: %d \n", bob.age);
}

Здесь определена функция print_person, которая через параметр получает структуру и выводит на консоль ее данные.

Определим файл app.asm со следующим кодом:

global main
 
extern print_person ; подключаем функцию print_person
 
section .data
name db "Bob", 0        ; поле name
age dq 43               ; поле age
id dq 111               ; поле id
 
section .text
main:
    sub rsp, 24           ; 24 байт для полей структуры

    mov rax, [id]
    mov [rsp], rax        ; передаем id

    lea rax, [name]
    mov [rsp+8], rax        ; передаем name

    mov rax, [age]
    mov [rsp+16], rax        ; передаем age

    call print_person 

    add rsp, 24              ; восстанавливаем стек
    ret

Здесь выделяем в стеке 24 байта - для трех полей (по 8 байт для каждого) и последовательно передаем в стек значения для полей. Причем первое поле (в примере поле id) передается на верхушку стека по адресу rsp, а все последующие поля размещаются после него

idrsp
namersp+8
agersp+16

Пример компиляции и работы программы:

root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o
root@Eugene:~/asm# gcc person.c  hello.o -static -o hello
root@Eugene:~/asm# ./hello
Id: 111
Name: Bob
Age: 43
root@Eugene:~/asm#

Передача указателя на структуру

Также функция Си может получать структуру через указатель:

#include <stdio.h>

struct person{int id; char* name; int age;};

void print_person(struct person* bob)
{
    printf("Id: %d \n", bob->id);
    printf("Name: %s \n", bob->name);
    printf("Age: %d \n", bob->age);
}

В этом случае код ассемблера через регистр (в данном случае первый параметр, значит через регистр RDI) передает адрес структуры:

global main
 
extern print_person ; подключаем функцию print_person
 
section .data
name db "Sam", 0        ; поле name
age dq 28               ; поле age
id dq 110               ; поле id
 
section .text
main:
    sub rsp, 24           

    mov rax, [id]
    mov [rsp], rax        ; передаем id

    lea rax, [name]
    mov [rsp+8], rax        ; передаем name

    mov rax, [age]
    mov [rsp+16], rax        ; передаем age

    mov rdi, rsp
    call print_person 

    add rsp, 24              ; восстанавливаем стек
    ret

Здесь в качестве области памяти выступает стек, а в регистр RDI передается адрес - значение регистра RSP.

Аналогично можно помещать данные в других областях памяти, например, в статической памяти:

global main     ; функция main - точка входа
 
extern print_person ; подключаем функцию print_person
 
section .data
name db "Sam", 0        ; поле name

id dq 110               ; поле id
namePtr dq name         ; указатель на name
age dq 28               ; поле age
 
section .text
main:
    sub rsp, 8           ; 8 байт - выравнивание

    mov rdi, id           ; загружаем начало области памяти
    call print_person 

    add rsp, 8              ; восстанавливаем стек
    ret

Здесь в регистр RDI загружается адрес переменной id, после которой идут указатель на переменную name и переменная age. Это позволяет сократить выделения памяти и также уменьшает количество применяемых инструкций на перемещение в стек. В то же время надо учитывать размер данных и выравнивание. Так, в данном случае все переменные занимают по 8 байт в сумме 24 байт, соответственно на стороне кода Си не возникнет проблем с получением данных.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850