LINUX.ORG.RU

ARM va_args GCC

 , , , ,


0

2

Решение сабжевой проблемы от Azq2: [Link] и iliyap: [Link]!


Железка ARMv4, ARM7TDMI-S. Код собирается старым ARM ADS компилятором и работает. Я решил собирать код православным GCC вместо проприетарного барахла: arm-none-eabi-gcc (10.3.1). В железке есть прошивка и она скомпилирована ADS. Функции прошивки я дёргаю из своего кода, который компилю GCC. Проверил – всё работает, кроме variadic functions. Параметры компиляции у ADS и у GCC следующие:

# ADS
/opt/arm/bin/tcc -c UtilLogStringData.c -o UtilLogStringData.o -bigend -apcs /interwork -O2

# GCC
arm-none-eabi-gcc -mabi=aapcs -ffreestanding  -mbig-endian -march=armv4t -mtune=arm7tdmi-s -mthumb -mthumb-interwork -O2 -nostdlib -fshort-wchar -fshort-enums -fpack-struct=4 -fno-builtin -c UtilLogStringData.c -o UtilLogStringData.o

Сама функция:

void UtilLogStringData(const char *format, ...) {
	char buffer[255];
	va_list vars;

	va_start(vars, format);
	vsprintf(buffer, format, vars);
	va_end(vars);

	suLogData(0, 0x5151, 1, strlen(buffer) + 1, buffer);
}

ADS на этом даёт вот такую картину:

10901958 <UtilLogStringData>:
10901958:	b40f      	push	{r0, r1, r2, r3}
1090195a:	b580      	push	{r7, lr}
1090195c:	b0c2      	sub	sp, #264	; 0x108
1090195e:	a845      	add	r0, sp, #276	; 0x114
10901960:	0880      	lsrs	r0, r0, #2
10901962:	0080      	lsls	r0, r0, #2
10901964:	9001      	str	r0, [sp, #4]
10901966:	a802      	add	r0, sp, #8
10901968:	aa01      	add	r2, sp, #4
1090196a:	9944      	ldr	r1, [sp, #272]	; 0x110
1090196c:	f765 fdcc 	bl	10867508 <vsprintf>
10901970:	2000      	movs	r0, #0
10901972:	9001      	str	r0, [sp, #4]
10901974:	a802      	add	r0, sp, #8
10901976:	f765 fd47 	bl	10867408 <strlen>
1090197a:	aa02      	add	r2, sp, #8
1090197c:	9200      	str	r2, [sp, #0]
1090197e:	2201      	movs	r2, #1
10901980:	1c43      	adds	r3, r0, #1
10901982:	2000      	movs	r0, #0
10901984:	49b7      	ldr	r1, [pc, #732]	; (10901c64 <loadELF+0x2ae>)
10901986:	f491 fd5d 	bl	10593444 <suLogData>
1090198a:	b042      	add	sp, #264	; 0x108
1090198c:	bc80      	pop	{r7}
1090198e:	bc08      	pop	{r3}
10901990:	b004      	add	sp, #16
10901992:	4718      	bx	r3

GCC выдаёт следующее, падение происходит на vsprintf:

109018f4 <UtilLogStringData>:
109018f4:	b40f      	push	{r0, r1, r2, r3}
109018f6:	b510      	push	{r4, lr}
109018f8:	b0c4      	sub	sp, #272	; 0x110
109018fa:	aa46      	add	r2, sp, #280	; 0x118
109018fc:	ca02      	ldmia	r2!, {r1}
109018fe:	ac04      	add	r4, sp, #16
10901900:	0020      	movs	r0, r4
10901902:	9203      	str	r2, [sp, #12]
10901904:	f765 fe00 	bl	10867508 <vsprintf>
10901908:	0020      	movs	r0, r4
1090190a:	f765 fd7d 	bl	10867408 <strlen>
1090190e:	2201      	movs	r2, #1
10901910:	1c43      	adds	r3, r0, #1
10901912:	9400      	str	r4, [sp, #0]
10901914:	2000      	movs	r0, #0
10901916:	4904      	ldr	r1, [pc, #16]	; (10901928 <UtilLogStringData+0x34>)
10901918:	f491 fd94 	bl	10593444 <suLogData>
1090191c:	b044      	add	sp, #272	; 0x110
1090191e:	bc10      	pop	{r4}
10901920:	bc08      	pop	{r3}
10901922:	b004      	add	sp, #16
10901924:	4718      	bx	r3
10901926:	46c0      	nop			; (mov r8, r8)
10901928:	00005151 	andeq	r5, r0, r1, asr r1

Так собственно вопрос, можно ли как-то заставить GCC генерить код так как это делает ADS, чтобы всё работало? В чём вообще причины того что всё отлетело? Я-то конечно забил ASM-костылём скопированным с ADS, но хочется чтобы GCC начисто генерировал корректный код.

★★★★★

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

Впрочем, по ссылке теоретически возможны проблемы: если функцию syscall() вызвали где-то в самом начале работы бинарника, с пустым стеком - va_arg() может обратиться за пределы верха стека и сделать сегфолт.

Вот видишь, начал понимать что такое UB и почему оно плохо :DDDD

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

Речь шла, разумеется, про блок, а не про весь исходник. Ты ещё расскажи, что перед объявлением локальных переменных функции в том же файле есть код ещё одной функции до неё и там тоже код.

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

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

Ты сам на ходу придумываешь какие-то правила «как надо». Почему этим правилам кому-то надо следовать, если ты сам не понимаешь, зачем они нужны?

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

А, да, тип va_list надо объявить указательным типом до того как прототип vsprintf объявлен. И правда без хедеров никак видимо. Хотя можно ещё попробовать (__va_list)&vars но это уже плохо смотрится.

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

Какой ещё счётчик? Он в cdecl ABI не предусмотрен, а реализовать его уровнем выше невозможно.

Да и срать?

если даже кто-то соорудит экзотическую архитектуру со счётчиком, он сам заметит что она несовместима с мейнстримными libc и пофиксит это ещё до её публиации.

Зачем? Смари, какой-нибудь Яббл выкатывает новую ОС для мобильников под название ГейРобот (конкурент Андроиду), и делает там вот такой вот va_arg() со счётчиком. Валидный софт портируется без проблем, невалидный падает в дупу, Яббл шлёт всех нахер, поскольку это не их проблема. Думаешь, такое невозможно? Ещё как возможно, яббл меняли архитектуру процессора, ABI и вообще всё подряд минимум раза два в последние 20 лет.

Ты, похоже, вообще мало представляешь, насколько крупным вендорам срать на совместимость. Особенно с сишным говнософтом.

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

Я имел ввиду экспорт этих функций

И как другие языки экспортируют свои функции (с переменным числом параметров) для других языков? Как вызвать из си хаскелевскую функцию (которая принимает переменное количество аргументов).

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

Как вызвать из ocaml, guile? Надеюсь не используются костыли на сишке, всё своё родное?

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

И как другие языки экспортируют свои функции (с переменным числом параметров) для других языков? Как вызвать из си хаскелевскую функцию (которая принимает переменное количество аргументов).

В хаскеле нет функций с переменным числом аргументов.

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

В хаскеле нет функций

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

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

Непонятно, что значит «падение происходит на vsprintf». То ли это падение на инструкции вызова vsprintf (bl vsprintf), то ли это падение внутри vsprintf.

Падение было внутри vsprintf, которая внутри прошивки, причём как я заметил – корёжилось оно только тогда, когда в vsprintf отправлялся аргумент со строчкой.

UINT32 loadELF(char *file_uri, char *params, void *Library, UINT32 reserve) {
	UtilLogStringData("ElfLdr Load Request %s", file_uri); // <= Креш далеко внутри `vsprintf()`
}
EXL ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

gcc и шланг уже знают. Там для этого костылей добавили. Смотри атрибут format здесь

Нет, не знают. Знает линтер встроенный в gcc/clang. Он не имеет отношения к кодогенератору и никак не влияет на него.

А ещё не все функции с переменным числом аргументов принимают форматную строку. Некоторые принимают количество аргументов первым параметром, некоторые ожидают, что последний аргумент NULL. И, наконец, форматная строка может создаваться в раниайме (например, грузиться из ресурсов перевода) - до тех пор пока она матчится с аргументами это не UB. Это только программист знает. А в линтер добавили выдачу предупреждений в паре частных случаев.

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

А ещё не все функции с переменным числом аргументов принимают форматную строку. Некоторые принимают количество аргументов первым параметром, некоторые ожидают, что последний аргумент NULL

А ещё можно сделать чтоб в аргументах передавались например опкоды (int), после которых некоторое количество аргументов, зависящее (и типы) от опкода, потом следующий опкод итд пока не встретится опкод 0. При этом нули в аргументах опкодов конца не обозначают. Распарсить такое можно только честно выполнив функцию.

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

Если честно, я скачал какой-то старый ADS для винды, установил и взял typedef int *va_list[1] оттуда. va_start там был определён через интринсик __va_start, но заменить его было не сложно. Такое определение va_list умеет работать только с аргументами в памяти.

Вообще arm32 calling convention использует для первых четырёх аргументов регистры r0, r1, r2, r3. Но если функция variadic, то gcc запихивает все эти регистры в стек, так что все аргументы получают адреса в памяти. Даже если variadic аргументы не используются в теле функции:

int f(int a) { return ~a; }
int g(...) { return 1; }
f:      mvns    r0, r0
        bx      lr
g:      push    {r0, r1, r2, r3}
        movs    r0, #1
        add     sp, sp, #16
        bx      lr

Сделано это как будто для совместимости с ADS, у которого va_list не умеет работать с аргументами из регистров. Почему при этом интринсик __gnuc_va_list получился несовместимым с ADS-ным typedef int *va_list[1] не понятно.

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

я скачал какой-то старый ADS для винды, установил и взял typedef int *va_list[1] оттуда. va_start там был определён через интринсик __va_start, но заменить его было не сложно.

Спасибо что заморочились. Понять как сравнить интринсики __va_start (ADS) и __builtin_va_start (GCC) между собой и органично перенести эти различия в макросы у меня не хватило опыта. 👍

Почему при этом интринсик __gnuc_va_list получился несовместимым с ADS-ным typedef int *va_list[1] не понятно.

Компилятор ADS очень старый, кажется 2001-2002 годы. Чуть позже после ADS (ARM Developer Suite) вышел более новый RVCT (RealView Compilation Tools), который похоже что в последних итерациях был уже совместим с GCC в этом моменте с variadic, хотя в доках RVCT есть такое:

The compiler generates errors like «Identifier va_list is undefined» when building GNU-style code.

This is because of slightly differing methods of implementing variadic functions between RVCT and GCC. The solution is to use the compiler option –preinclude stdarg.h to include the definitions of these types before the start of the application code.

AN150B_Building_Linux_Applications_with_RVCT.pdf

Но это к версии 3.0 заметка. На 4.0 уже походу нормально всё.

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

Нет, не знают. Знает линтер встроенный в gcc/clang. Он не имеет отношения к кодогенератору и никак не влияет на него.

Этот «линтер» – не часть компилятора?

наконец, форматная строка может создаваться в раниайме (например, грузиться из ресурсов перевода) - до тех пор пока она матчится с аргументами это не UB.

Я тоже люблю программы, которые свои данные не только не валидируют – они просто не могут этого делать кроме как упав в корку. Молодец, так держать!

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

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

При этом нули в аргументах опкодов конца не обозначают. Распарсить такое можно только честно выполнив функцию.

Мне вспомнилась небольшая заметка о том, что perl нельзя распарсить статически. По мне, это означает, что пердл – просто чудовищный говноязычок, фанатов которого нельзя подпускать к компьютерам даже близко. Но некоторые перловики реально гордятся этой фичей: мол, это позволяет писать на перле просто потрясающий по своей хитров&!%*^^@|сти код, который потом смогут прочитать и понять лишь избранные. Вот ты что-то аналогичное из мира Си описал.

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

Почему при этом интринсик __gnuc_va_list получился несовместимым с ADS-ным typedef int *va_list[1] не понятно.

Почему получился то понятно: они и не пытались сделать его совместимым, а традиции его объявления оказались отличающимися. На совместимость самих vararg функций это, разумеется, никак не влияет, так что это две разные задачи. Впрочем, логично было бы подумать и про это.

А суть несовместимости вот в чём: в gcc va_list это указатель на аргумент а памяти, который ты следующий будешь вытаскивать, а в ads это указатель на переменную, где хранится этот указатель. Отличие подходов в том, что после вызова того же vsprintf с переменным его ads-style va_list-ом, этот va_list указывает на хвост, оставшийся после забранных vsprintf-ом аргументов (если он есть), а в случае gcc этот va_list после вызова vsprintf указывает туда же, куда и до вызова. Возможно, у обеих сторон были причины придерживаться именно такой логики работы.

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

а в случае gcc этот va_list после вызова vsprintf указывает туда же, куда и до вызова

Нет, это не так. Иначе вот такой код выводил бы не 0 hello, а 0 (null):

#include <stdarg.h>
#include <stdio.h>
void f(const char* f1, const char* f2, ...)
{
    va_list ap;
    va_start(ap, f2);
    vprintf(f1, ap);
    vprintf(f2, ap);
    va_end(ap);
}
int main()
{
    f("%ld ", "%s\n", 0L, "hello");
    return 0;
}

va_list только выглядит как передаваемый по значению. На самом же деле он передаётся по указателю. То есть скорее всего является вектором из одного элемента, как в ADS: typedef void *va_list[1]. Сишные вектора ведь передаются по указателю, даже если вектор из одного элемента.

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

Это на arm так?

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

Собственно у меня он 0 (null) и выдаёт - на i386. В amd64 вероятно по-другому устроено т.к. там сложности из-за передачи аргументов в регистрах.

является вектором

Никогда не понимал нафига с++-ники приняли эту дурацкую моду называть массивы векторами.

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

Хм, действительно:

// va_type.cpp
#include <stdarg.h>
#include <type_traits>
static_assert(std::is_object<va_list>::value);
static_assert(std::is_pointer<va_list>::value);
static_assert(std::is_array<va_list>::value);

На x86 va_list является указателем:

$ g++ -m32 -c va_type.cpp
va_type.cpp:5:39: error: static assertion failed
    5 | static_assert(std::is_array<va_list>::value);

На x86_64 va_list является вектором:

$ g++ -c va_type.cpp
va_type.cpp:4:41: error: static assertion failed
    4 | static_assert(std::is_pointer<va_list>::value);

И действительно, на x86 получается 0 (null), на на x86_64 получается 0 hello.

Вобщем va_list это непредсказуемое говно. Пойду поплачу.

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

Оно очень предсказуемое – у тебя есть интерфейс к нему, и ты можешь пользоваться только им, если хочешь портируемости. Попытки сишников постоянно забраться в детали реализации и потом страдать что все сломалось – классика жанра. А ещё в С нет нормального ABI, поэтому попытки скрестить компиляторы, которые напрямую не заявляют тесную совместимость – заканчиваются примерно вот так.

gaylord
()
Последнее исправление: gaylord (всего исправлений: 2)