Структуры представляют удобную организацию данных и нередко применяются в качестве параметров или результата функций на 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 мы получим стек
id | rsp |
name | rsp+8 |
age | rsp+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, а все последующие поля размещаются после него
id | rsp |
name | rsp+8 |
age | rsp+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 байт, соответственно на стороне кода Си не возникнет проблем с получением данных.