Форум программистов, компьютерный форум, киберфорум
Markus_13
Войти
Регистрация
Восстановить пароль
Блог быдлокодера Markus'а
Почитал? Иди поешь! xD
Рейтинг: 5.00. Голосов: 6.

Динамическая загрузка DLL из ресурса на примере Bass.dll

Запись от Markus_13 размещена 24.10.2013 в 01:56
Обновил(-а) Markus_13 12.08.2014 в 05:00 (забыл добавить исправленный MemModule)

  • Небольшое вступление:
Часть 0

Какими вообще мотивами нужно руководствоваться для использования столь нестандартного метода загрузки ДЛЛ посредством проекции её в память?

Прежде всего - это один из способов защиты. К примеру: мы можем динамически загружать длл с сервера во время работы программы (также мы можем использовать некоторые трюки для защиты и обфускации памяти приложения или добавить шифрование/сжатие кода и динамическую распаковку-дешифровку в памяти) - таким образом наша гипотетическая дллка с сервера не оставит никаких следов в системе (да и даже при дебаге благодаря некоторым ухищрениям с релоками и адресным пространством, этот метод сделает жизнь потенциальному реверсеру немного насыщенней и интересней =)))

Вторая причина - удобство конечного пользователя: согласитесь, проще иметь 1 независимый exe, чем кучу отдельных модулей и файлов.

Третий вариант - инжектирование в сторонние приложения, да-да, вы не ослышались очитались)) это вполне реально! (сам не верил =)

А вообще, если честно, это просто интересно и познавательно с программерской точки зрения =)))

  • Используем BTMemoryModule:
Часть 1

Что это такое? Это модуль для Delphi, содержащий функции для загрузки DLL из виртуального пространства памяти.
Скачать можно там: http://delphibasics.info/home/... morymodule
Скажу сразу: all credits goes to Martin Offenwanger (coder[at]dsplayer.de) and Joachim Bauch (mail[at]joachim-bauch.de)
Martin Offenwanger - этот чувак переписал модуль под дельфу
а этот: Joachim Bauch написал оригинальный код на Сях
Лицензия модуля открытая: GNU Lesser General Public License (что не суть, т.к. он опенсорс=)

Но вам можно вообще не заморачиваться и скачать мое демо-приложение с модифицированным модулем. (о правках чуть ниже)

Так или иначе, предположим что у вас есть этот модуль =) Как же его юзать? Весьма просто...))
Если вы знакомы с динамической загрузкой dll - можете сразу перейти к следующей вехе моего повествования =))

Список объявленных функций (я рассматриваю свою версию модуля с правками):
Delphi
1
2
3
4
5
6
7
8
// return value is nil if function fails
function MemLoadLibrary(var f_data: Pointer; const f_size: int64; fin: byte = FIN_BOTH; attach: boolean = true): PMemModule; stdcall;
// return value is nil if function fails
function MemGetProcAddress(var f_module: PMemModule; const f_name: PAnsiChar): Pointer; stdcall;
// free module
procedure MemFreeLibrary(var f_module: PMemModule); stdcall;
// returns last error
function MemGetLastError: AnsiString; stdcall;

MemLoadLibrary - аналог винапишной LoadLibrary, но грузит либру из памяти.
Входные параметры:
f_data - указатель на область памяти, содержащую длл
f_size - размер (длина) области
fin - набор флагов для финализации (добавлен мной) - об этом параметре речь пойдет ниже
attach - булка, определяющая вызов EP (DllMain) внутри функции загрузки (добавлено мной) - думаю тут особых комментариев не нужно, если не хотим авто вызов мэйна - просто ставим false
далее можем получить адрес суммой .codeBase + .headers.OptionalHeader.AddressOfEntryPoint

MemGetProcAddress - убираем префикс "Mem" и... да, это тоже аналог винапишной функции =)
Входные параметры:
f_module - ссылка на структуру _BT_MEMORY_MODULE
f_name - имя функции, все прозрачно

MemFreeLibrary - ага, очередной доппельгангер, даже комментировать не буду))

MemGetLastError - позволяет получить последнее сообщение об ошибке, при получении оно затирается (добавлено мной)

Структура MemModule:
Delphi
1
2
3
4
5
6
7
  _BT_MEMORY_MODULE = packed record
    headers: PImageNtHeaders;
    codeBase: Pointer;
    modules: Pointer;
    numModules: integer;
    initialized: boolean;
  end;
Ну вот, теперь все ясно, не так ли?) Если нет - смотрим VCL пример вызова функции из либры прилинкованной в виде ресурса:
Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
procedure MemDllCall;
var
  rm: tResourceStream;
  mem: pointer;
  dlSz: int64;
  mDLL: PMemModule;
  SomeFunc: procedure(i:integer);
begin
  try
    rm:= tResourceStream.Create(hInstance,'MyDLL',RT_RCDATA);
    rm.Position := 0;
    dlSz:=rm.Size;
    mem := GetMemory(dlSz);
    rm.Read(mem^, dlSz);
    rm.Free;
  except
    ShowMessage('Someshit happened while RES load: ' + SysErrorMessage(GetLastError));
    if rm <> nil then rm.Free;
    exit;
  end;
  try
    mDLL:= MemLoadLibrary(mem, dlSz);
    if mDLL <> nil then @SomeFunc := MemGetProcAddress(mDLL, 'SomeFunс');
    if @SomeFunc <> nil then SomeFunc(123);
  except
    ShowMessage('Someshit happened while DLL load: ' + MemGetLastError);
  end;
  if mDLL <> nil then MemFreeLibrary(mDLL);
  FreeMemory(mem);
end;
Хотя в общем случае я бы порекомендовал производить загрузку и получение адресов функций единожды - при инициализации, а не каждый раз перед вызовом.

Часть 2

Модуль написан весьма неплохо и изящно (на мой вкус=) но при этом кое-что я все таки поправил. (в коде все мои правки помечены)
Не буду заострять внимание на мелочах, отмечу лишь один аспект. Это финализация секций. Код вызова соотв-ей функции предваряется комментарием:
Цитата:
// mark memory pages depending on section headers and release
// sections that are marked as "discardable"
По 1му пункту вопросов нет - проставляем флаги доступа на области памяти в соотв-ии с флагами секций с помощью VirtualProtect - все четко и понятно.
Но вот насчет "discardable" секций - вопрос спорный. Оговорюсь: я не профи в лоу лвл кодинге и не могу ничего утверждать наверняка, и даже более того: конкретно в этом вопросе я не шарю вовсе.
Но, тем не менее, следующий код вызывает нарекания, как в теории, так и на практике:
Delphi
1
2
3
4
5
6
  if (l_section.Characteristics and IMAGE_SCN_MEM_DISCARDABLE) <> 0 then begin
      // section is not needed any more and can safely be freed
      VirtualFree(Pointer(l_section.Misc.PhysicalAddress), l_section.SizeOfRawData, MEM_DECOMMIT);
      inc(longword(l_section), sizeof(TImageSectionHeader));
      continue;
  end;
Начнем с теории: во1ых (если верить гуглу и людям замеченным в распространении инфы на данную тему) флаг IMAGE_SCN_MEM_DISCARDABLE актуален лишь для сервисов и драйверов, а не аппликух и либр;
во2ых (если верить msdn) этот флаг никак не претендует на показатель необходимости вайпать помеченные им секции, а всего лишь говорит что они МОГУТ быть удалены (но, опять же, с учетом того что при необходимости они могут загрузиться физически из файла) что в контексте нашей задачи не сильно актуально =)

На практике: уже 4 дллки при проекции и очистке Discardable секций успешно скрошИлись скрАшились, причем некоторые из удаляемых секций были с IMAGE_SCN_MEM_EXECUTE - т.е. содержали код.

Я, конечно, априори воспринимаю создателя этого модуля как кодера с квалификацией выше моей (как минимум в этой теме), но если на практике что-то не работает - увы и ах, но пришлось править код =)

Как упоминалось выше в функцию загрузки добавлен параметр fin (:byte) - он содержит флаги для финализации секций:
Delphi
1
2
3
4
5
{ SectionFinalization Flags: }
  FIN_NOFIN    = $0;//no SectionFinalization
  FIN_PROTECT  = $1;//SectionFinalization with VirtualProtect
  FIN_DISCARD  = $10;//SectionFinalization with Discard
  FIN_BOTH     = $11;//FIN_PROTECT + FIN_DISCARD
по умолчанию в функции указан флаг FIN_BOTH - т.е. фактически при этом флаге функция финализации выполняется в оригинальном виде, если указать FIN_DISCARD - будет производиться лишь выпиливание Discardable секций, если указать FIN_NOFIN - функция финализации не будет вызываться вообще, я рекомендую использовать FIN_PROTECT (по крайней мере для сторонних библиотек) - при этом флаге будет производиться присваивание протекции страницам памяти, но проверка флага IMAGE_SCN_MEM_DISCARDABLE будет пропускаться.

Вопрос о том что делать с Discardable-секциями пока остается открытым (для меня уж точно).
Поэтому если вы располагаете информацией по этой теме - ощутите свободу прокомментировать (feel free to comment =))
Или можно написать мне на мыло: iam[at]markus13.name


  • Демо приложение использующее проекцию библиотеки Bass в памяти:
Часть 3

Приложение демонстрирует использование описанного метода на практике: оно воспроизводит ogg-файл с помощью BASS.DLL (и файл, и длл`ка хранятся внутри экзешника как прилинкованные ресурсы и динамически фиксируются в памяти для использования).

Прежде всего находим ogg-файл и библиотеку bass (рекомендую юзать ту, что в архиве вместе с сорцами моего демо-приложения, т.к. нормальная басс.длл проверяет свою целостность ;)
Пишем в текстовом файле:
Код:
dll RCDATA "bass.dll"
snd RCDATA "%SomeSound%.ogg"
переименовываем его в "BassMem.rc" и открываем в "...\Delphi7\Bin\brcc32.exe" чтобы скомпилить RES-файл

Создаем новый консольный проект и копипастим в него код:
Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
////////////////////////////by Markus_13////////////////////////////////////////
program BassMem; { App demonstrating dynamic BASS usage via Memory Projection }
uses Windows,strUtilz,MemModuleUnicode;
//strUtilz - мой модуль с выпиленными asm функ-ми из SysUtils (inttostr/strtoint,etc...)
 
const//КОНСТАНТЫ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  _s='   ';
  _n=#13#10;//разрыв строки
  ver='1.0';//версия
  tit=' Bass Memory App '+ver;//title - название приложения
  msgYN=$04; msgERR=$10; msgINF=$40; //<-коды типов сообщений
  res1='dll';//название ресурса с дллкой
  res2='snd';//название ресурса со звуком
  //всякий трэш:
  mrFail='Unable to load res: ';
  bt1='Pause';
  bt2='Play';
  btX='Close';
  WM_SETICON=$80;
 
type//ТИПЫ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  MemRes=record//структура для проекции ресурса в память
    p:pointer;//указатель на область памяти
    sz:int64;//размер (длина)
    rd:cardinal;//hResData
    ri:cardinal;//hResInfo
  end;
 
var//ПЕРЕМЕННЫЕ:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  wnd:cardinal;//хэндл окна
  ss:cardinal;//хэндл звукового потока
  snd:MemRes;//указатель на звуковой файл в памяти
  dll:MemRes;//указатель на дллку в памяти
  bass:PMemModule;//структура проекции длл в памяти
  stp:word;//шаг выполнения (для дебага)
  st:boolean;//статус звукового потока
  th:cardinal;//хэндл потока для замены кнопок
  ti:cardinal;//id потока
//объявляем функции из bass.dll:::::::::::::::::::::::::::::::::::::::::::::::::
  BASS_Init:function(device: integer; freq, flags: cardinal; win: cardinal; clsid: pGUID):bool; stdcall;
  BASS_Free:function: bool; stdcall;
  BASS_StreamCreateFile:function(mem: bool; f: Pointer; offset, length: int64; flags: cardinal): cardinal; stdcall;
  BASS_StreamFree:function(handle: cardinal): bool; stdcall;
  BASS_ChannelPlay:function(handle: cardinal; restart: bool): bool; stdcall;
  BASS_ChannelStop:function(handle: cardinal): bool; stdcall;
  BASS_ChannelPause:function(handle: cardinal): bool; stdcall;
 
{$R BassMem.RES} //ресурс со звуком и либрой
{$R XPMan.res} //ресурс с иконкой и манифестом
 
////////////////////////////////////////////////////////////////////////////////
//ФУНКЦИИ:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 
function mesaga(m:string;f:longint=0):integer;//выдача сообщения
begin
  result:=MessageBox(wnd,pchar(m+_s),tit,f+$1000);
end;
 
procedure mResFree(mr:MemRes);//освобождаем ресурс
begin
  mr.p:=nil;
  mr.sz:=0;
  UnlockResource(mr.rd);//для совмести с 16-bit Win
  FreeResource(mr.rd);//для совмести с 16-bit Win
  mr.rd:=0;
  mr.ri:=0;
end;
 
function Res2Mem(hInst:cardinal;res:string;rtype:pAnsiChar):MemRes;//проецируем ресурс в память
begin
  result.p:=nil;
  result.ri:=FindResource(hInst,pchar(res),rtype);//находим ресурс
  if result.ri=0 then exit;
  result.sz:=SizeOfResource(hInst,result.ri);//передаем размер в резалт
  if result.sz=0 then exit;
  result.rd:=LoadResource(hInst,result.ri);//загружаем ресурс в память
  if result.rd=0 then exit;
  result.p:=LockResource(result.rd);//фиксируем ресурс в памяти и передаем указатель на него в резалт
end;
 
function mResValid(mr:MemRes):boolean;//проверка валидности проекции ресурса
begin
  result:=(mr.rd>0)and(mr.ri>0)and(mr.p<>nil)and(mr.sz>0);
end;
 
procedure quit(e:string='');//процедура выхода (e - месага ошибки)
begin
  if e<>'' then begin
    e:='Error: '+e+'!'+_s+_n+_s+'Step = '+inttostr(stp);//компонуем месагу
    if MemGetLastError<>'' then//добавляем строку ошибки из MemModule:
      e:=e+_s+_n+_s+'DllMem_Error: '+MemGetLastError;
    mesaga(e,msgErr);//выдаем
  end;
  //освобождаем хэндлы, ресурсы и т.д. (в соответствии с шагом выполнения):
  if stp>=90 then begin
    BASS_ChannelStop(ss);//останавливаем звуковой поток
    BASS_StreamFree(ss);//освобождаем звуковой поток
  end;
  if stp>=70 then BASS_Free;//отключаем bass
  if stp>=80 then mResFree(snd);
  if stp>=60 then MemFreeLibrary(bass);//выгружаем либру
  if stp>=50 then mResFree(dll);
  if th>0 then begin
    if ti>0 then TerminateThread(th,0);
    CloseHandle(th);
  end;
  halt(ord(e<>''));//завершаемся
end;
 
function DlgModify(p:pointer):boolean;//функция замены текста на кнопках в месседжБоксе
var
  w:cardinal;//хэндл диалога
  i:cardinal;//хэндл кнопки
  s:pchar;//строка для замены текста
  c:word;//счетчик
begin
  w:=0;
  c:=0;
  while(w=0)and(c<999)do begin//ждем появления мсгБокса
    sleep(25);
    w:=FindWindow(WC_DIALOG,tit);
    inc(c);
  end;
  if w>0 then begin
    SendMessage(w,WM_SETICON,ICON_SMALL,LoadIcon(hInstance,IDI_APPLICATION));//ставим иконку
    i:=GetDlgItem(w,IdYes);//находим кнопку "Дa"
    if i>0 then begin
      if st then s:=bt1 else s:=bt2;
      SetWindowText(i,s);//пишем "Play/Pause" в соотв-ии со статусом
    end;
    i:=GetDlgItem(w,IdNo);//находим кнопку "Нет"
    if i>0 then begin
      s:=btX;
      SetWindowText(i,s);//пишем "Close"
    end;
  end;
  ExitThread(0);//закрываем поток
  ti:=0;
  result:=true;
end;
 
////////////////////////////////////////////////////////////////////////////////
var sts:shortString;//строка статуса
begin///////////////////////////////////////////////////////////////////////////
  wnd:=0;
  {(можно дописать создание нормального окна с кнопками, но мне лень =)
  поэтому wnd=HWND_DESKTOP, а UI реализуем через MsgBox`ы ниже =)}
  ti:=0;
  stp:=0;
  dll:=Res2Mem(hInstance,res1,RT_RCDATA);//грузим ресурс с либрой в память
  if not mResValid(dll) then quit(mrFail+res1)else stp:=50;//проверяем^
  bass:=MemLoadLibrary(dll.p,dll.sz,FIN_PROTECT);//загружаем либру
  {(юзаем FIN_PROTECT чтобы Discardable секции не вайпались при финализации,
  подробней в статье - http://cyberforum.ru/blogs/14360/blog1682.html)}
  if(bass=nil)or(MemGetLastError<>'')then quit('Unable to load BASS DLL')else stp:=60;//проверяем^
  //получаем адреса функций:
  @BASS_Init:=MemGetProcAddress(bass,'BASS_Init');
  @BASS_Free:=MemGetProcAddress(bass,'BASS_Free');
  @BASS_StreamCreateFile:=MemGetProcAddress(bass,'BASS_StreamCreateFile');
  @BASS_StreamFree:=MemGetProcAddress(bass,'BASS_StreamFree');
  @BASS_ChannelPlay:=MemGetProcAddress(bass,'BASS_ChannelPlay');
  @BASS_ChannelStop:=MemGetProcAddress(bass,'BASS_ChannelStop');
  @BASS_ChannelPause:=MemGetProcAddress(bass,'BASS_ChannelPause');
  //проверяем^:
  if(@BASS_Init=nil)or(@BASS_Free=nil)or(@BASS_StreamCreateFile=nil)or(@BASS_StreamFree=nil)
or(@BASS_ChannelPlay=nil)or(@BASS_ChannelStop=nil)or(@BASS_ChannelPause=nil)then quit('Some of GetProcAddress failed');
  //инициализируем bass и выходим в случае неудачи:
  if not BASS_Init(-1,44100,0,wnd,nil)then quit('Unable to init BASS')else stp:=70;
  snd:=Res2Mem(hInstance,res2,RT_RCDATA);//грузим ресурс со звуком в память
  if not mResValid(snd) then quit(mrFail+res2)else stp:=80;//проверяем^
  ss:=BASS_StreamCreateFile(true,snd.p,0,snd.sz,4{=BASS_SAMPLE_LOOP});//создаем звуковой поток из памяти
  if ss=0 then quit('MemStream creation failed')else stp:=90;//проверяем^
  BASS_ChannelPlay(ss,false);//запускаем звуковой поток (начинаем воспроизведение)
  st:=true;//статус = true = звук проигрывается
  while(true)do begin//реализуем взаимодействие с юзером и делаем вид что у нашей проги есть окно =)))
    if st then sts:='Playing...' else sts:='Paused.  ';
    th:=CreateThread(nil,0,@DlgModify,nil,0,ti);//создаем поток для модификации диалога (месседжБокса)
    if mesaga(' Sound currently '+sts,msgInf+msgYN)=IdNo then break//закрываемся по кнопке "Нет"
    else begin//инверсируем статус по кнопке "Да"
      st:=not st;
      //останавливаем / продолжаем воспроизведение звука в соотв. со статусом:
      if st then BASS_ChannelPlay(ss,false) else BASS_ChannelPause(ss);
      if ti>0 then TerminateThread(th,0);//убиваем поток, если он жив (по невероятному стечению обстоятельств =)
      CloseHandle(th);//закрываем хэндл потока
      th:=0;
    end;
  end;
  quit;//выходим =)
end.
Думаю комментариев хватит.

Немного арифметики:
мой ogg-файл: 919 КБ
подправленная bass.dll: 106 КБ
XPMan.res (иконка + манифест): 3 КБ
= 1028 КБ (1.004 МБ)
EXE-файл после чистки мусора и паковки UPX (коэффициент никакой, т.к. ogg-файл с LZ-сжатием и либра, которую тоже особо не сожмешь =) весит 1049 КБ - разница 21 КБ (имхо неплохо)
Можно еще попробовать использовать wav-файл: сам файл будет больше естес-но, и памяти при проекции будет больше жрать, но возможно в некоторых случаях это окупится итоговым размером экзешника после паковки.

Доп. комментов по коду писать не буду - и так кучу времени убил, а у меня сейчас цейтнот =\

З.Ы. strUtilz кстати вообще лучше выпилить из uses и вставить в код функцию "inttostr" из SysUtils. Или убрать те места, где она юзается =))


Upd: Если у вас не работает данный метод (или работает с FIN_BOTH) - пишите ОС, проц (архитектуру) и разрядность в комменты. Я пока тестил только на Win7 AMD x64 - все пашет.

................................................................................ ................................................................................ .....
Все файлы демо-приложения + ресурсы + bass.dll + MemModule с правками во вложении:
Вложения
Тип файла: zip BassMemApp.zip (3.00 Мб, 888 просмотров)
Размещено в Без категории
Показов 14682 Комментарии 9
Всего комментариев 9
Комментарии
  1. Старый комментарий
    [QUOTE]подправленная bass.dll:[/QUOTE]
    Это как? =-O
    Запись от размещена 24.10.2013 в 02:22
  2. Старый комментарий
    Аватар для Markus_13
    Цитата:
    Сообщение от angstrom Просмотреть комментарий
    Это как?
    там отключена проверка целостности
    Запись от Markus_13 размещена 24.10.2013 в 02:32 Markus_13 вне форума
  3. Старый комментарий
    Я не про это. Снимал защиту с DLL?
    Запись от размещена 24.10.2013 в 04:37
  4. Старый комментарий
    Аватар для Markus_13
    Цитата:
    Сообщение от angstrom Просмотреть комментарий
    Я не про это. Снимал защиту с DLL?
    нет, не я))
    Запись от Markus_13 размещена 24.10.2013 в 17:56 Markus_13 вне форума
  5. Старый комментарий
    Аватар для Markus_13
    Извиняюсь, когда запостил эту статью - я забыл включить в архив исправленный MemModule. Перезалил.

    З.Ы. хотя для тех кто внимательно и вдумчиво читал статью, я думаю, было не сложно исправить кусок кода с проверкой DISCARDABLE-флага и освобождением секции =)
    Delphi
    1
    
    if (l_section.Characteristics and IMAGE_SCN_MEM_DISCARDABLE)...
    Запись от Markus_13 размещена 12.08.2014 в 05:05 Markus_13 вне форума
  6. Старый комментарий
    Аватар для Markus_13
    Martin Offenwanger добавил в модуль флаги финализации секций, обновленную версию ищите там: http://code.google.com/p/memor... Module.pas
    GitHub: https://github.com/dsplayer/memorymodule
    Запись от Markus_13 размещена 12.08.2014 в 05:05 Markus_13 вне форума
    Обновил(-а) Markus_13 28.02.2016 в 07:09 (upd)
  7. Старый комментарий
    Спасибо народ !
    У меня все получилось, но с небольшим исключением. В приведенном вами примере 'BassMemApp' использовался метод 'MemGetProcAddress'. Но у меня все получилось только с 'BTMemoryGetProcAddress'
    Еще раз спасибо! Интересная тема...
    Запись от kk_123 размещена 26.01.2019 в 16:33 kk_123 вне форума
  8. Старый комментарий
    Аватар для Rius
    Цитата:
    Вторая причина - удобство конечного пользователя: согласитесь, проще иметь 1 независимый exe, чем кучу отдельных модулей и файлов.
    Ага, часто это обоснование слышно. Только от программистов, а не от пользователей, почему-то. Пользователи же как ставили программы инсталляторами, так и ставят, и на количество файлов им пофигу - они туда даже не смотрят.
    Запись от Rius размещена 26.01.2019 в 21:12 Rius вне форума
  9. Старый комментарий
    Аватар для Avazart
    Цитата:
    проще иметь 1 независимый exe, чем кучу отдельных модулей и файлов.
    Чем проще? Вообще непонятно откуда берутся такие утверждения.
    Запись от Avazart размещена 27.01.2019 в 17:27 Avazart вне форума
    Обновил(-а) Avazart 27.01.2019 в 17:28
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru