Ассемблер в *nix – удел извращенца..?


A lot of people ask me..
stupid fuckin questions.
A lot of people think that..
ассемблера в *nix не существует.
(С) типа Eminem

Да, асм в *nix таки существует. Просто многие в это почему-то отказываются верить. Данная статья скромно претендует развеять все мифы и загадки вокруг этого загадочного явления. Не ищите здесь подробного описания AT&T синтаксиса и системных вызовов – я просто попробую описать те трудности и невзгоды, которые обязательно придется преодолеть смельчаку, желающему полностью овладеть *nix-ом через асм (в смысле — научиться программировать на асме под *nix, прим. для CyberManiac-а 🙂 ).

Миф I:

“Синтаксис асма под *nix в корне отличается от оного под DOS/WIN, он кривой и к нему невозможно привыкнуть”.

Действительно, синтаксис, предложенный компанией AT&T;, немного (безусловно, в лучшую сторону) отличается от Intel’овского. Но это ничего не значит. На самом деле, если бы все еще с детства начали кодировать, используя AT&T; синтаксис, то интеловский очень скоро загнулся бы и не дожил до наших дней. Просто AT&T; в свое время не стремилась делать свои поделки достоянием народных масс, это прерогатива MS и Intel (а вообще у AT&T; уже существовала своя развитая культура и традиции, когда MS и Intel только спускались с деревьев 🙂 ).

А уж если кто-то переборол свои страхи и все же перешел к AT&T; с Intel, того точно уже за уши не оттянешь назад. По этому поводу когда-то даже родился проект DJGPP (GNU Binutils) — асм с AT&T; синтаксисом под DOS (//www.delorie.com/djgpp/). Однако в силу непонятных причин проект не прижился.

Если все эти доводы кому-то показались слишком хлипкими и невразумительными, то нет ничего проще — //sf.net/projects/nasm/. Качайте себе NASM под *nix и будьте с Intel «вместе навсегда».

Миф II:

“Асм под *nix никому не нужен и вообще все это изврат, С – вот единственная дорога в светлое будущее”.

Существует известная в широких кругах поговорка: «Линукс писан программистами для программистов». На самом деле полностью она звучит так: «Линукс писан сишными программистами для сишных программистов». И с этим никто не спорит. Дело в том, что любому, кто попытается сунуться в такой монастырь как *nix со своим уставом (асм), тому суждено упереться в огненные стены и рвы с крокодилами (об этом ниже). Однако не все так трагично – уменьшение размера кода в сто и более раз, увеличение производительности в десятки раз, да и чего греха таить, – само удовольствие, которое может доставить только кодинг на живом языке — все это стоит того чтобы научиться асму под *nix.

Теперь, возможно это кого-то сильно удивит, но программирование на асме под *nix по своему стилю очень напоминает… программирование на оном под DOS (да! Именно под DOS). Многие скажут: *nix – полностью 32-х разрядная ОС, и работает в защищенном режиме, используя flat-модель памяти. При чем здесь 16-битная ОС реального режима DOS? Про такого с уверенностью можно сказать: он просто никогда не писал на асме под *nix. На самом деле можно сделать еще более шокирующее заявление: писать программы на асме в *nix НАМНОГО проще чем под DOS… Самое важное – не сойти “с пути истинного” в самом начале его…

#1: когда только начинаешь писать на асме под *nix то возникает интересное ощущение: вроде бы ты попал в грязный пятибаксовый мотель (из тех, возле которых обязательно проходит метро и когда едет поезд на потолке дрожит дешевая люстра и мигает свет); здесь давно нет горячей воды, обои уродливыми клочьями свисают со стен, с потолка капает какая то мерзкая гадость и пахнет плесенью, все удобства – во дворе… На мотеле (подпертые кем-то неизвестным) стоят уже давно покосившиеся со временем неоновые буквы «*NIX для ассемблерщиков» (половина букв давно не горит, а половина с треском догорает). У мотеля нет своих постояльцев. Сюда заезжают лишь переночевать, чтобы на следующее утро убраться подальше…

Самое мерзкое во всем этом то, что через единственное окно в этой конуре, через дорогу, как будто специально, вырос семизвездочный отель, весь в рекламе, бассейнах и пальмах… Прямо над входом (к которому то и дело поминутно подъезжают все более и более крутые тачки) сверкает золотом надпись: “*NIX для сишников”. Вон видно как по террасам ходят пузатые мужики в обнимку с дорогими бабами, потягивая коктейли и куря сигары, им прислуживает армия официантов и слуг; все они смеются и живут.

Всем им наплевать на мотель напротив…

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

Я говорю к тому, что ассеблерщик, сунувшийся в *nix не найдет практически никакой документации, описывающей системные вызовы на низком уровне. Здесь (//www.lxhp.in-berlin.de/lhpsyscal.html) об этом можно получить кое-какую захудалую информацию, но многие функции описаны неправильно, либо вообще не описаны. Иногда порядок расположения параметров в регистрах при передаче в ту или иную функцию приходиться подбирать буквально вручную, методом научного тыка. Но на самом деле настоящего асм-кодера все это может только раззадорить..

Для вызова любой системной функции используется команда INT 80h (вспомните DOS – там для этой цели использовался INT 21h). Параметры передаются через регистры. Номер функции – в АХ. Вся проблема в том, что найти полное описание того, в каком регистре какой параметр и для чего передается крайне проблематично, ресурс, указанный выше частично решает эту проблему.

Когда я говорил про все неземные блага, которые предоставляются для сишников, я имел ввиду полную документированность любой запятой, с которой только можно встретиться в увлекательном процессе программирования на С под *nix.

#2: вот это действительно самый большой фак: за всю историю существования *NIX никто не написал ни одного стоящего отладчика асм-кода (а может и написал, но не захотел поделиться с общественностью). Все что удалось найти (был перерыт буквально весь Интернет и опрошены десятки знающих людей) – ALD (Assembly Linux Debugger). Все что про него можно сказать – да, он действительно чем-то круче MS debug-а. Вот только чем именно — сказать довольно сложно. Все остальные отладчики дальше С-шного кода ничерта не видят. Писать программы без отладчика (а тем более на асме) – это верх извращенческого гения.

Конечно, существует еще и скромный аналог сайса под Линукс – PrivateICE (//sourceforge.net/projects/pice). Единственная проблемка – на последних версиях ядер Линукса он не компилируется (вот вам и переносимость С).

Прим.: 9 июня 2003 года на sourceforge появился новый билд pice-а. Однако вот что пишет сам автор:Since this project was abandoned a few years ago there is no active maintainence. [skipped]. The files provided here are as it is and there is no garuantee that they will compile. [skipped]. Of course you are welcome to send in bug reports or comments on this code, just don’t expect that they can be compiled out of the box. 🙂 These sources will compile on 2.4.18. They might give problems with non-SMP enabled kernels.

Скомпилировать так ничего и не удалось :). Если кто-то вдруг найдет заклинание, по которому pice можно скомпилировать под последние ASP, Mandrake или RedHat– сообщите плз на ящик внизу.

#3: проблема заголовочных файлов. Чуть ли не большую часть времени желающему покодить на асме в *nix придется провести за увлекательным поиском необходимой информации по заголовочным файлам. Искать придется практически все буквенные названия, которые могут встретиться в процессе (это и названия самих системных вызовов, и параметров, передаваемых в них, и вообще любых переменных). Все они как будто специально порастасканы по тысячам *.h – файлов, которые находятся в самых неожиданных местах. Сишнику в семизвездочном отеле достаточно всего лишь щелкнуть пальцем и сделать include ….h – все работает. Ассемблерщику в мотелишке напротив – сначала понять к чему тот или иной параметр, затем устроить поиск по всем системным директориям, найти заголовочный файл, содержащий этот параметр, затем скопировать его в «свой», который будет понимать GAS или NASM (или, если ассемблерщик попался реальный, то запомнить его, и везде использовать численные значения параметров ?), а в довершение еще и усадить в правильный регистр перед отправкой в недра функции.

#4: прога, написанная для *nix на асме теряет переносимость на другие *nix-платформы. Для перекомпиляции под другую *nix платформу придется изрядно повозиться, в некоторых случаях проще переписать заново весь код, чем переделывать старый с Linux под BSD. Но BSD пока не так распространен, а самые попсовые версии линуксов (Red Hat, Mandrake, ASP) используют одно и то же ядро, и данный фак вообщем-то не так уж страшен как его малюют.

Ну вот, а теперь вы сами убедитесь, что писать программы на асме под Линукс не сложнее чем под DOS, а может даже и проще.

Рассмотрим пример написания простейшего клиент-серверного приложения, использующего в качестве взаимодействия стек протоколов TCP/IP (подразумевается, что вы более-менее знакомы с сетевым программированием, и знаете хотя бы, что такое сокет).

Клиентское приложение посылает серверу символьную строку; Сервер шифрует символьную строку по следующему алгоритму шифрования: выполняет замену одного символа – буквы, на символ располагающийся на две позиции правее в алфавите, для последнего и предпоследнего символов в алфавите выполняется кольцевой сдвиг, например, для «Y» это будет буква «А» для «Z» — «В» (это шифр Цезаря). Сервер отправляет зашифрованное сообщение обратно; Клиент выбирает шифрограмму и выводит на экран.

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

Ну что ж, начнем с сервера. Я по ходу пьесы попытаюсь максимально комментировать происходящее. Начну с того, что наш сервер будет демоном (для пущего понту). Что такое демон? Демон на языке DOS-ассемблерщиков – резидент. Все. Так что ничего страшного.

# *******************************************************
# Server (daemon)
# by Broken Sword [HI-TECH]
# (for Linux based on Intel x86 only)

# brokensword@mail.ru
# www.wasm.ru

# Compile Instructions:
# -------------------------------------------------------
# as server.s
# ld --strip-all -o server a.out
# *******************************************************

# *******************************************************

# этот файлик содержит выдранные из какого-то файла
# определения системных вызовов (см. FUCK #3)
.include		"syscalls.inc"		
# а этот – все остальные, которые только могут 
# встретиться

.include 	"def.inc"		        
		.text					    # начало сегмента кода
# метка, с которой все начинается (нужно чтоб она была 
# глобальной)
		.globl		_start		 	

_start:							
# итак, начинаем лепить нашего демона
# процесс создания демона в *.nix и создание резидента в DOS в корне различаются
# начинается любой демон с того, что нужно создать дочерний процесс.
# Создать дочерний процесс в линуксе проще 
# пареной репы – достаточно поместить номер сис.
# вызова в EAX и сделать «а-ля int 21h», т.е. int 80h
		movl		$SYS_fork,%EAX
		int		$0x80		
# все. 
# Теперь у нас параллельно сосуществуют ДВА процесса:
# родительский (в котором исполнялись все предыдущие 
# команды) и дочерний. Что же содержит дочерний код? 
# А все то же самое, что и родительский.
# Т.е. важно понять, что # весь нижеследующий (и выше тоже)
# код находиться в памяти в ДВУХ разных местах.
# Как процессор переключается между 
# ними (и всеми остальными живыми процессами) 
# – читайте «Переключение задач» в интеловском мануале. 
test		%EAX,%EAX		
# вот эту команду необходимо осознать.
# Прежде всего, важно понять, что данная команда
# существует и в родительском 
# и в дочернем процессах (об этом выше).
# Следовательно выполниться она и там и там.
# Все дело в том, что после 
# int 80h родительскому процессу вернется PID сына
# (в EAX ессесно, вообще все возвращается в EAX, как и в винде)
# а что же вернется сыне? Правильно, нолик.
# Именно поэтому следующий jmp будет выполнен
# в дочернем процессе и 
# не будет выполнен в родительском.

# ребенок улетает на метку _cont1
		jz		_cont1			
# ...а в это время, в родительском процессе:
		xorl		%EBX,%EBX		# EBX=status code
		xorl		%EAX,%EAX		#
		incl		%EAX			# SYS_exit 
# завершаем родительский процесс.
		int		$0x80			

# Теперь все дети 
# управляются процессом INIT


_cont1:
		movl		$SYS_setsid,%EAX
# сделаем нашего ребенка главным в группе
		int		$0x80			
		
		movl		$1,%EBX			# SIGHUP
		movl		$1,%ECX			# SIG_IGN
		movl		$SYS_signal,%EAX
# далее сигнал SIGHUP будет игнорироваться
		int		$0x80			
							
		movl		$SYS_fork,%EAX
# наш ребенок уже подрос и теперь сам может родить сына
		int		$0x80			
# (по сути – это уже внук нашему изначальному 
# родительскому процессу)

# EAX=0 в дочернем и EAX=PIDдочернего в родительском

		test		%EAX,%EAX		

jz		_cont2			

# внук нашего родительского (которого уже давно нет в 
# живых) улетает на метку _cont2, однако отец все еще 
# жив!!! (все как в мексиканском сериале)
		
		xorl		%EBX,%EBX		# EBX=status code
		xorl		%EAX,%EAX		#
		incl		%EAX			# SYS_exit
		int         $0x80			

# вот уже и отец отправлен к деду на небеса (да, 
# злостная программка, недаром демоном зовется)

# ..а в это время внучок получает все наследство и 

_cont2:

# продолжает жить
# далее, после того,
# как все кровавые разборки и отцеубийства благополучно завершены,
# внучок,продавший душу демону, 
# преспокойно создает сокет.
# Дело в том, что в линуксе есть такие системные вызовы,
# для вызова которых их номер не 
# помещается в EAX.
# Вместо этого в EAX помещается номер функции-мультиплексора,
# реализующий вызов конкретной 
# функции номер которой помещается в EBX.
# Так, например, происходит при вызове IPC и SOCKET-функций.
# Кроме того, 
# при вызове SOCKET-функций параметры располагаются не в регистрах,
# а в стеке. Смотри как все просто:

		pushl		$0			# протокол
		pushl		$SOCK_STREAM		# тип

		pushl		$AF_INET			# домен
# ECX должен указывать на кадр в стеке, содержащий 
		movl		%ESP,%ECX		
# параметры, такая уж у него судьба...
		movl		$SYS_SOCKET,%EBX		
# а вот это уже номер той самой конкретной функции
# SOCKET – создать сокет
							
		movl		$SYS_socketcall,%EAX
	
# в EAX - номер функции мультиплексора (по сути он 
# просто перенаправит вызов в функцию, указанную в EBX
		
       int         $0x80

# сокет создан! Ура товарищи. В EAX возвратиться его дескриптор.

# «очистим» стек (по сути это выражение придумано специально
# для HL-программистов, на самом деле ничего не 
# очищается, данную операцию необходимо производить только
# для того чтобы в дальнейшем не произошло переполнение 
# стека, но в таких маленьких программках это делать вовсе
# не обязательно):

		addl		$0xC,%ESP

		movl		%EAX,(sockfd)		

# сохраним дескриптор созданного сокета в переменной
# sockfd

# далее необходимо осуществить операцию BIND,
# которая называется «привязка имени сокету»,
# хотя суть этого названия 
# слабо отражает смысл происходящего на самом деле.
# На самом деле BIND просто назначает конкретному сокету IP-
# адрес и порт, через который с ним можно взаимодействовать:

# размер передаваемой структуры (вообще подобран 
		pushl		$0x10
# методом тыка, потому что логически непонятно почему 
# именно 16)

# указатель на структуру sockaddr_in

       pushl		$sockaddr_in
# дескриптор нашего сокета
		pushl		%EAX			
# ECX указывает на параметры в стеке
		movl		%ESP,%ECX		
# номер функции BIND – в EBX
		movl		$SYS_BIND,%EBX
# функция-мультиплексор
		movl		$SYS_socketcall,%EAX
		int		$0x80
# теперь сокет «привязан» к конкретному IP-шнику и порту
# поднимем ESP на место
		addl		$0xC,%ESP		
# далее что-либо подробно описывать я не вижу смысла,
# любой желающий сам без труда разберется, опираясь на 
# полученные выше знания.
		pushl		$0			# backlog
		movl		(sockfd),%EAX
		pushl		%EAX
		movl		%ESP,%ECX
		movl		$SYS_LISTEN,%EBX
		movl		$SYS_socketcall,%EAX
		int         $0x80
		addl		$0x8,%ESP

_wait_next_client:
		pushl		$0			# addrlen
		pushl		$0			# cliaddr
		movl		(sockfd),%EAX
		pushl		%EAX			# sockfd
		movl		%ESP,%ECX
		movl		$SYS_ACCEPT,%EBX
		movl		$SYS_socketcall,%EAX
		int		$0x80
		addl		$0xC,%ESP

		movl		%EAX,(connfd)
		
		movl		$SYS_fork,%EAX
		int		$0x80			# create child process
		test		%EAX,%EAX
		jnz		_wait_next_client

_next_plain_text:
		movl		(connfd),%EBX
		movl		$buf,%ECX		# ECX->buf
		movl		$1024,%EDX		# 1024 bytes
		movl		$SYS_read,%EAX
		int		$0x80			# wait plain_text

		movl		$buf,%ESI
		movl		%ESI,%EDI
		movl		%EAX,%ECX
		movl		%EAX,%EDX
_encrypt:
		lodsb
		cmp		$0x41,%AL		# A
		jb		_next
		cmp		$0x5A,%AL		# Z
		ja		_maybe_small
		incb		%AL
		incb		%AL			# encryption ;)
		cmp		$0x5A,%AL
		jle		_next
		sub		$26,%AL
_maybe_small:
		cmp		$0x61,%AL		# a
		jb		_next
		cmp		$0x7A,%AL		# z
		ja		_next
		incb		%AL
		incb		%AL			# encryption ;)
		cmp		$0x7A,%AL
		jle		_next
		sub		$26,%AL
_next:
		stosb
		loop		_encrypt
		
		movl		(connfd),%EBX
		movl		$buf,%ECX		# ECX->chiper_text
		movl		$SYS_write,%EAX
		int		$0x80			# send plain_text
		
		jmp		_next_plain_text
# *****************************************************
		.data
sockfd:		.long		0
connfd:		.long		0
sockaddr_in:	
sin_family:	.word		AF_INET
sin_port:	.word		0x3930			# port:12345
sin_addr:	.long		0			# INADDR_ANY
buf:
# *****************************************************



#Клиент пишется по аналогии с сервером,
# думаю сами без труда разберетесь:


# *********************************************************
# Client
# by Broken Sword [HI-TECH]
# (for Linux based on Intel x86 only)

# brokensword@mail.ru
# www.wasm.ru

# Compile Instructions:
# ---------------------------------------------------------
# as client.s
# ld --strip-all -o client a.out
# *********************************************************

# *********************************************************		
       .include		"syscalls.inc"
		.include 	"def.inc"
		.text
		.globl		_start
_start:
		pushl		$0			# protocol
		pushl		$SOCK_STREAM		# type
		pushl		$AF_INET			# domain
		movl		%ESP,%ECX
		movl		$SYS_SOCKET,%EBX
		movl		$SYS_socketcall,%EAX
		int		$0x80
		addl		$0xC,%ESP

		movl		%EAX,(sockfd)

		pushl		$0x10			# addrlen
		pushl		$sockaddr_in
		pushl		%EAX			# sockfd
		movl		%ESP,%ECX
		movl		$SYS_CONNECT,%EBX
		movl		$SYS_socketcall,%EAX
		int		$0x80
		addl		$0xC,%ESP

_next_plain_text:
		xorl		%EBX,%EBX		# stdin
		movl		$buf,%ECX		# ECX->buf
		movl		$1024,%EDX		# 1024 bytes
		movl		$SYS_read,%EAX
		int		$0x80			# read from stdin

		movl		(sockfd),%EBX
		movl		$buf,%ECX		# ECX->plain_text
		movl		%EAX,%EDX		# bytes read
		movl		$SYS_write,%EAX
		int		$0x80			# send plain_text

		movl		$SYS_read,%EAX
		int		$0x80			# wait chiper_text
		
		xorl		%EBX,%EBX
		incl		%EBX			# EBX=1 (stdout)
		movl		$SYS_write,%EAX
		int		$0x80			# disp chiper_text
		
		jmp		_next_plain_text
# *********************************************************
		.data
sockfd:		.long		0
sockaddr_in:	
sin_family:	.word		AF_INET
sin_port:	.word		0x3930			# port:12345
sin_addr:	.long		0x0100007F		# 127.0.0.1
buf:
# *********************************************************

Вот так вот все просто (вся сложность на самом деле заключена в понимании стека TCP/IP, а не в том, как закодировать все эти действия на асме).

Все — запускайте бесов server, а за ним client.

p.s. использовать данное приложение для шифрования важных данных на диске не рекомендуется ).

Благодарности (в алфавитном порядке :)):

Aquila [HI-TECH]
CyberManiac [HI-TECH]
Edmond [HI-TECH]
Vladimir [HI-TECH]

…и всей группе HI-TECH! Держитесь, ребята!

Примеры к статье client server

[C] Broken Sword

 

Источник WASM.RU /01.09.2003/

Поделиться в соц сетях

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Есть идеи, замечания, предложения? Воспользуйтесь формой Обратная связь или отправьте сообщение по адресу replay@sciencestory.ru
© 2017 Истории науки. Информация на сайте опубликована в ознакомительных целях может иметь ограничение 18+