Рассылка закрыта
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
Низкоуровневое программирование для дZенствующих (FAQ)
НИЗКОУРОВНЕВОЕ ПРОГРАММИРОВАНИЕ ДЛЯ ДZЕНСТВУЮЩИХ (FAQ) АРХИВ MAIL-КОНФЕРЕНЦИИ RTFM_HELPERS ЗА ФЕВРАЛЬ 2001 Сайт: http://hi-tech.nsys.by/ Информация о конференции: http://hi-tech.nsys.by/forum/ ИО модератора aka DZ Kir777: kir777@hotmail.com Co-moderator aka Serrgio: serrgio@gorki.unibel.by [В О П Р О С - О Т В Е Т] ======================================================================= ВОПРОС: Кста что значит комманда jnz, мы ее не проходили, а в 13 выпуске в оптимизированном коде графического редактора она есть, или это очередная очепятка и это вовсе не jnz, jne? Было бы еще хорошо, если бы в каждом выпуске если появляется новая комманда разъясняли, что она делает, а то я вот долго думал на какие такие 3 буквы нужны комманды dec и inc и что они делают, потом подумалось, что одна отнимает из значения хранящегося в стеке 0001h, другая прибавляет 0001h. YOSHI: JNZ значит абсолютно то же, что JNE. JNE - это мнемокод ассемблера(NASM/MASM). При ассемблировании она "превращается" в JNZ. JNZ(Jump if Not Zero) и JNE (Jump if Not Equal) - абсолютно одинаковы. Попробуй в debug'e ассемблировать эти две команды - увидишь. Эти команды определяют, надо ли делать переход по значению флагов процессора(О флагах в следующих выпусках рассылки, как говорил Serrgio) Например, команда cmp ax,12h проверяет, равенство AX на 12h путем вычитания одного операнда из другого. После этой операции она устанавливает нужные флаги. Если AX действительно равен 12h, то Zero Flag (флаг нуля) установится в единицу. Команды JNE и JNZ, в зависимости от значения этого флага делают переход. Если ZF = 0, то переходят, если ZF = 1, то не переходят. JE и JZ - наоборот, если ZF = 0, то не переходят, если ZF = 1, то переходят. DEC - декрементирует(уменьшает на единицу значение ее операнда) INC - инкрементирует(увеличивает на единицу значение ее операнда) xor ax,ax ; AX = 0 inc ax ; AX = 1 inc ax ; AX = 2 dec ax ; AX = 1 dec ax ; AX = 0 ----------------------------------------------------------------------- ВОПРОС: Возник у меня вопрос: ну,например, есть у меня в DX циферки (кол-во кластеров или еще что-то);как их вывести на экран? Ведь это не байт,а слово!Я пробовал пересылать в сегмент данных, а потом - через mov ah,09; int 21h выводить на экран. Что-то не получается.Подскажите! 2K: Необходимо 1. перевести циферки (в DX) в буковки :) т.е. в соответствующие символы : 0 --> 48 ( ASCII 48 = '0' ) 1 --> 49 .. 9 --> 57 2. сформировать строку из этих символов 3. вывести : mov ah,09 int 21h Ну , короче, вот что получилось: MODEL tiny DATASEG stro db 5 dup (' '),'$' ; тут формирование строки CODESEG STARTUPCODE mov dx,12334 ; выводимое число (по условию в DX) mov ax,dx mov bp,4 mov bx,0Ah ; для перевода в dec n_ch: xor dx,dx div bx ;ax=ax/bx , dx -- остаток add dx,48 ;можно так : or dx,30h mov [stro+bp],dl ; очередной символ dec bp ;подготовка к следующ. символу cmp ax,bx ;может хорош уже делить? jg n_ch ;если ax>10, то еще немножко поделим add al,48 ;перевод последнего числа mov [stro+bp],al ; ну и запись lea dx,stro ; mov ah,9 ;вывод int 21h ; int 20h ;выход END ЭТО вполне можно еще оптимизировать ----------------------------------------------------------------------- ВОПРОС: Расскажите кто знает как извлечь корень. Это вообще можно сделать не применяя разложение в ряд? BELTSY: да, вполне можно. вообще этих способов до фига. ну вот, че первое приходит в голову, метод Герона (или Ньютона, где как): если хотим вычислить x=a^(1/2), a>0, x>0, то X<n+1> = (a/X<n> + X<n>) / 2 <n> - индекс, x0 - на глаз выбераешь. пример: 2^(1/2), x0 = 1.5, получим x1 = 1.417.... x2 = 1.414216..., x3 = 1.41421635624..... при хорошем x0 сходимость не плохая. ----------------------------------------------------------------------- ВОПРОС: Разрабатываю платку ввода информации по шине PCI на базе PLX9050. Может кто подскажет, как работать с локальной памятью, если адрес 32 разрядный (под DOSом)? Вот например, нужно мне залить данные в память по адресу 0xE4001000. Как это сделать ? Если можно то подробнее . OLEGH: Просто так нельзя... адресуемая память в реальном режиме процессора ограничена 1-м мегабайтом... разве что если немного подDZенствовать... Тогда и горы можно свернуть... :) Вообще-то лучше использовать стандартный DOS-расширитель, напимер DOS4G(W) или PharLap, или какой другой... Для DOS4GW лучше всего писать на Watcom C + ASSEMBLER (куда же без ASM-а...). Я пробовал - мне понравилось... Есть (у меня где-то были) и очень простые DOS-расширители, созданные в разное время разными людьми (любителями), размером 1-2 kB, но для них нужно писать проги только на ASM-е... Можно и вызывать DPMI-хост - под виндами он встроенный, а под досом можно взять из QEMM. Дока по вызовам DPMI - в любом справочнике по прерываниям - например, в Ральфе Брауне... Но я такой способ не пробовал... Преимуществом использования DOS-расширителя является работоспособность написанной проги под различными конфигурациями - под EMM, QEMM, Windows 95/98,... Недостатком - Вы все-таки работаете в защищенном режиме процессора, в режиме 32-х-разрядной адресации, а там своя специфика... Но это все была лирика, теперь перейдем к делу (см. первый абзац). Как известно, DZенствующий народ легких путей не ищет... СУЩЕСТВУЕТ СПОСОБ в РЕАЛЬНОМ режиме работы процессора использовать 32-х-битные адреса и адресоваться ко всем 4-м гигабайтам адресного пространства. Для этого нужно: 1) естественно, использовать в командах процессора 32-битные адреса и (при необходимости) операнды - примеры команд: .386 mov ebx, 0E4001000h mov ax, [0E4001003h] add eax, [ebx] mov eax, [ebx + ecx*4 + 3] ; на 386+ можно даже так !!! Нужно иметь в виду, что все указанные команды компилируются и могут быть выполнены в РЕАЛЬНОМ режиме процессора. Другое дело, что попытка выполнения команды, содержащей обращение по 32-разрядному смещению, значение которого превышает 0x0000FFFF (т.е. 64 К), вызовет исключительную ситуацию процессора под названием ... ммм... кажется, General Protection Violation, номер прерывания 0Dh. Причина - выход за установленные пределы размеров сегментов, которые по умолчанию равны 64 К. Обработчик прерывания 0D ничего не делает, возвращается - а возврат происходит на НАЧАЛО этой же иструкции, вызвавшей исключение. В результате программа виснет, зацикливаясь на одной инструкции. А чтобы не висла, см. ниже. Также, будучи скомпилированными в 16-битный кодовый сегмент (а именно в такой сегмент мы будем компилироваться для работы под DOS) инструкции, содержащие 32-битные операнды/адреса, будут дополнены специальными префиксами смены разрядности - 066h и 067h. 2) открыть (разрешить) адресную линию A20. По умолчанию она аппаратно закрыта)отключена, чем достигается совместимость с 8086. Это можно сделать, вызвав соответствующую функцию из драйвера XMS. 3) и главное - путем предварительного специального программирования процессора увеличить пределы всех сегментов до 4 Гб. Это делается переключением в защищенный режим, установкой специально подготовленной GDT и возвратом в реальный режим. Несмотря на кажущуюся сложность, весь код умещается на одну страницу... Та же GDT имеет всего две строчки... Одна из которых пустая :) Этот способ РЕАЛЬНО РАБОТАЕТ. Я писал большие программы (целые системы), использующие этот способ. Более того - стандартный для DOS драйвер XMS - himem.sys - использует именно этот способ для работы с "верхней" памятью. Достоинства метода - простота и высокая эффективность. Недостатки - программа будет работать только в реальном режиме, т.е. не должно быть драйверов EMM, QEMM, а только XMS (т.е. himem.sys). И уж конечно - никаких форточек. Если этот метод кого-то заинтересует, могу подельться опытом и сырцами. NICK: Есть один истинно DZенский способ :) Правда сразу предупреждаю, что проверялся только на Intelовских кирпичах. Итак рассказываю: 1. Переходим в защищенный режим. 2. Записываем в сегментые регистры сегменты по 4Гб. 3. Возвращаемся в реальный режим. Отсюда идет "обычная" программа. 4. Обращаемся к памяти используя 32-х битные регистры. Теперь возвращаем все обратно. 5. Переходим в защищенный режим. 6. Записываем в сегментные регистры сегменты размером 64Кб. 7. Возвращаемся в реальный режим. Если никто ничего не понял :), то вот это на ассемблере. .model small .code .386p MyGdt db 0, 0, 0, 0, 0, 0, 0, 0 db 0FFh, 0FFh, 0, 0, 0, 93h, 8Fh, 0 db 0FFh, 0FFh, 0, 0, 0, 93h, 0, 0 MyGdtr dw 18h dw 0, 0 ; int SetupBig(void) ; Returns: 0 - OK, 1,2 - Error: Already in PM or VM public _SetupBig _SetupBig proc far db 66h smsw ax test eax,80000001h jnz err1 xor eax,eax xor edx,edx mov ax,cs mov dx,offset MyGdt shl eax,4 add eax,edx mov [MyGdtr+2],ax shr eax,16 mov [MyGdtr+4],ax xor ax,ax ret err1: test ax,1 mov ax,1 jnz err2 mov ax,2 err2: ret _SetupBig endp ; void SwitchBig(void) public _SwitchBig _SwitchBig proc far cli push ds push es mov dx,8 lgdt qword ptr [MyGdtr] mov eax,cr0 or al,1 mov cr0,eax jmp short $+2 mov ds,dx mov es,dx mov fs,dx mov gs,dx mov eax,cr0 and al,0FEh mov cr0,eax jmp far ptr l1 l1: xor ax,ax mov ds,ax mov es,ax pop es pop ds sti ret _SwitchBig endp ; void SwitchSmall(void) public _SwitchSmall _SwitchSmall proc far cli push ds push es mov dx,10h lgdt qword ptr [MyGdtr] mov eax,cr0 or al,1 mov cr0,eax jmp short $+2 mov ds,dx mov es,dx mov fs,dx mov gs,dx mov eax,cr0 and al,0FEh mov cr0,eax jmp far ptr l2 l2: xor ax,ax mov ds,ax mov es,ax pop es pop ds sti ret _SwitchSmall endp ; char ReadBigByte(long addr) public _ReadBigByte _ReadBigByte proc far push bp mov bp,sp push si push es xor ax,ax mov es,ax mov esi,[bp+6] mov al,es:[esi] pop es pop si pop bp ret _ReadBigByte endp ; void ReadBigByte(long addr,char val) public _WriteBigByte _WriteBigByte proc far push bp mov bp,sp push si push es xor ax,ax mov es,ax mov esi,[bp+6] mov al,[bp+10] mov es:[esi],al pop es pop si pop bp ret _WriteBigByte endp end Все функции готовы для употребления из C. ----------------------------------------------------------------------- ВОПРОС: Есть вопрос относительно адресации в памяти. В рассылке все про адресацию все понятно. Но в некоторых справочных материалах я нашел совершенно жуткие надписи.Например, мне совершенно непонятно, как будет выглядеть в dZebug'е адрес "0000:0x7c00". Ну ведь адрес записывается так: сегмент:смещение. И сегмент и смещение - по 4 байта (Например 0000:0100 - все ясно). Но ведь здесь еще какой-то "х" стоит!!! На MOV DX,0x7c00 дебаг жутко ругается. Подскажите, пожалуйста, как такой адрес в записывать. YOSHI: Префикс "0x" означает число в шестнадцатеричной системе. 0000:0x7C00 - это одно и то же, что и 0000:7C00h. Просто в некоторых программах(и других языках программирования, как C++ или Perl), запись 0x7C означает число 7С в шестнадцатеричной системе, т.е. 7Сh. Поэтому, в DEBUG'e пиши просто 7C00. AB> На MOV DX,0x7c00 дебаг жутко ругается. Пиши просто: mov dx,7C00 ----------------------------------------------------------------------- ВОПРОС: При запуске *.com в регистрах ax,bx,cx,dx,si,di,bp обязательно нули, или возможны случаи когда что-то другое? ХЕМУЛЬ: Цитата из TechHelp 6.0 (второй рулез, обязательный к наличию у любого программера), раздел Program Startup & Exit: -¦All Programs¦- Before either a COM- or EXE-format program is loaded, DOS selects a segment address, called the PSP (Program Segment Prefix) as the load base [...] ¦ It sets the AX register to indicate the validity of the drive IDs (if any) of the filespec parameters entered on the command line: if AL=0ffH, then the first drive ID was invalid if AH=0ffH, then the second drive ID was invalid [...] -¦COM Programs¦- COM-format programs define a single segment. The bytes of the COM file [...] Compilers and linkers may refer to COM programs as those using the "tiny" memory model. ¦ CS, DS, ES, and SS are set the same as the PSP. ¦ SP is set to the end of the PSP segment (usually 0fffeH, but it will be lower if a full 64K is not available). The word at offset 06H of the PSP is set to indicate how much of the program segment is available. ¦ All system memory above the Program Segment is allocated to the program. ¦ A word of 00H is pushed onto the stack. ¦ IP is set to 100H (the first byte of the load module) by a JMP to PSP:100 Таким образом, при старте .COM файла определены сегментные регистры CS/DS/ES/SS (все равны адресу PSP), регистры IP (100h), SP (0FFFEh, если только свободной памяти, куда загружена прога, - например, в UMB, не меньше 64K) и AX. Остальные регистры не определены и зависят от окружающей обстановки: например, я слышал, по значению BX определяют запуск под Turbo Debugger. Также, любой файл может быть упакован, и тогда значение регистров будет зависеть от кода распаковщика. ----------------------------------------------------------------------- ВОПРОС: Обьясните, плиз, работу idiv и imul. ZERO-Q: idiv- операция деления двух двоичных значений со знаком. Для команды необходимо задание двух операндов ? делимого и делителя. Делимое задается неявно, и размер его зависит от размера делителя, местонахождение которого указывается в команде: *если делитель размером в байт, то делимое должно быть расположено в регистре ax. После операции частное помещается в al, а остаток ? в ah; *если делитель размером в слово, то делимое должно быть расположено в паре регистров dx:ax, причем младшая часть делимого находится в ax. После операции частное помещается в ax, а остаток ? в dx; если делитель размером в двойное слово, то делимое должно быть расположено в паре регистров edx:eax, причем младшая часть делимого находится в eax. После операции частное помещается в eax, а остаток ? в edx; imul-операция умножения двух целочисленных двоичных значений со знаком. Алгоритм работы команды зависит от используемой формы команды. Форма команды с одним операндом требует явного указания местоположения только одного сомножителя, который может быть расположен в ячейке памяти или регистре. Местоположение второго сомножителя фиксировано и зависит от размера первого сомножителя: если операнд, указанный в команде, ? байт, то второй сомножитель располагается в al; если операнд, указанный в команде, ? слово, то второй сомножитель располагается в ax; если операнд, указанный в команде, ? двойное слово, то второй сомножитель располагается в eax. Результат умножения для команды с одним операндом также помещается в строго определенное место, определяемое размером сомножителей: при умножении байтов результат помещается в ax; при умножении слов результат помещается в пару dx:ax; при умножении двойных слов результат помещается в пару edx:eax. Команды с двумя и тремя операндами однозначно определяют расположение результата и сомножителей следующим образом: в команде с двумя операндами первый операнд определяет местоположение первого сомножителя. На его место впоследствии будет записан результат. Второй операнд определяет местоположение второго сомножителя; в команде с тремя операндами первый операнд определяет местоположение результата, второй операнд ? местоположение первого сомножителя, третий операнд может быть непосредственно заданным значением размером в байт, слово или двойное слово. Схема команды: imul множитель_1 imul множ_1,множ_2 imul рез-т,множ_1,множ_2 Argument needs type override-Требуется явно указать тип операнда. Требуется явно указать размер (тип) выражения, так как транслятор не может сделать этого, исходя только из контекста. Отметим лишь, что такого рода ошибки исправляются с помощью оператора PTR, позволяющего сообщить транслятору истинный размер операнда. Illegal immediate-Недопустим непосредственный операнд. Материал приведен из e-книги: "Справочная система по языку ассемблера IBM PC". Автор Юров Виктор Иванович. ----------------------------------------------------------------------- ВОПРОС: Скажите, чем отличается команда "lea" от команды "mov" и почему в некоторых случаях первая ассемблируется как вторая? ХЕМУЛЬ: LEA - это загрузка в регистр адреса второго аргумента, MOV - загрузка в регистр значения второго аргумента. В случае "прямой адресации" инструкция LEA идентична по сути, но _медленнее_, чем MOV с аргументом константой, равное адресу в инструкции LEA (почему её многие трансляторы, в том числе и TASM, обычно и преобразуют в соответствующий MOV, где возможно). Примеры: const equ 3 var dw ? mov ax,const ; загрузить в AX значение CONST mov ax,[var] ; загрузить в AX значение переменной var (адрес которой будет определён компоновщиком) mov ax,offset var ; загрузить в AX адрес переменной var lea ax,[var] ; загрузить в AX адрес переменной var Обратите внимание: переменная - это значение этой переменной и адрес в памяти, где это значение расположено. Имя переменной - это "абстрактный адрес", который при трансляции программы в исполняемый код преобразуется в число-адрес памяти. Обратите внимание: для получения адреса var использована директива offset (смещение). Тут два момента: 1. помимо прочих недостатков интеловских ассемблеров, у них традиционно интерпретация инструкции "mov ax,var" зависит от определения var - если она определена через equ или =, то это считается загрузкой константы, а если она определена через db, dw и т.п., то загружается значение переменной, расположенной по адресу var. Если мы хотим загрузить значение переменной с фиксированным адресом, то должны указывать скобки (например, "mov ax,[40]"), а если хотим загрузить адрес, то должны указать offset (как показано выше). С другой стороны, читать инструкцию "mov ax,var" тяжело, поскольку приходится вспоминать, как определена var, либо листать сорс в поисках определения - поэтому рекомендуется всегда указывать скобки или offset, в явном порядке. 2. эта директива названа offset потому, что мы берём смещение переменной от начала сегмента памяти, который в общей памяти может быть расположен где угодно. Это - частный случай понятия "контекста", когда любой объект (а также их набор) всегда определяется в некотором родительском контексте и адресуется в его рамках и по его правилам. Например, файлы "адресуются" по именам, сектора - по головка/трек/сектор, объект в памяти - сегмент:смещение и т.п. На самом деле, не все виды LEA могут быть закодированы _одной_инструкцией другого вида. Например, "lea ax,[bx+si]" загрузит в AX сумму BX и SI (и при этом не поменяет значение флагов!), а это можно изобразить только двумя другими инструкциями MOV/ADD (хотя, на самом деле, обычно это будет быстрее исполняться - такова особенность интеловских процов). В общем случае, адрес может состоять из трёх частей, соответственно LEA позволяет просуммировать до трёх аргументов (хотя, на самом деле, в адресе могут участвовать не все регистры и не в любой комбинации!): "lea ax,[bx+si+2]". Такая избыточность - не редкость. Например, инструкция "NOP" имеет тот же код, что и инструкция "XCHG AX,AX". С другой стороны, инструкция "MOV AX,[var]" может быть странслирована в два разных кода, причём второй короче, поскольку специально разработан для загрузки значения переменной в AX. ----------------------------------------------------------------------- ВОПРОС: Имеется ли в АСМе команда сгенерить случайное число? VIPER: Парочка соображений по поводу генератора, которого нету, но который можно сделать. Начнем с конца. Предgоложим, пара DX:AX содержит случайное число. Делим эту штуку на твой (диапазон+1) и в остатке получаем случайное число из этого диапазона. div BX ; Это как раз и есть твой диапазон ; После этого в AX случайное число из диапазона. Ну здесь все просто и понятно. Теперь о том как в DX:AX Получить просто случайное число.... Воспользуемся функцией 2Ch int 21 :-) Только воспользуемся умно. xor bx,bx ; Здесь будем получать наше случайное число mov ah,2ch int 21h and dl,0fh ; берем только "младшую" часть :-) mov bh,dl ; сохраняем ее в bx shl bh,4 ; освобождаем место под второе "случайное" число m1: loop m1 ; хорошенько потусуем колоду int 21h ; получаем второе случайное число and dl,0fh ; берем только "младшую" часть :-) mov bh,dl ; сохраняем ее в bx m2: loop m2 int 21h ; третье число and dl,0fh ; берем только "младшую" часть :-) mov bl,dl ; сохраняем ее в bx shl bl,4 ; освобождаем место под четвертое "случайное" число m3: loop m3 int 21h ; получаем четвертое случайное число and dl,0fh ; берем только "младшую" часть :-) mov bl,dl ; сохраняем ее в bx и т.д.... Если увеличить количество чтения времени (во я выразился... 8-() ) и за раз писать не 4, а 2 бита (или вообще 1) - тогда полученое число будет еще случайней. :-) Можно извратнуться по другому... То что системные часики тикают 18.2 раза в секунду - это конечно, факт, но факт покалебимый. В компутере есть микросхемка (не помню точно как называется, что-то типа 8253.., ну это и не важно. Главное есть.) которая генерит сигналы с частотой ~1.19 МГц. Там же есть 16-разрядный регистр. С каждым приходом cигнала от микросхемки (которая 1.19 МГц) значение регистра увеличивается на 1. И как только значение в регистре достигнет максимума (0FFFFh)- тикает системный таймер. Если 1.19МГц разделим на 0FFFFh то и получим 18.2 Гц. Вот откуда взялась эта непонятная циферка 18.2. :-) Отсюда вывод: если мы запишем в регистр, например 0FFh (вместо 0FFFFh), то системные часики будут тикать в 0FFh раз быстрее (256 для тех кто не понял :-) ) Если считаете что это мало - ну тогда пишите в регистр 0Fh. Часики будут тикать в 4 тысчи раз быстрее. Прошла секунда, а по компутерным часам целый час. Все понятно? Тогда начинаем ломать таймер. :-) cli ; Запретим прерывания mov al,3ch out 43h,al mov al,0ffh ; младший байтик out 40h,al ; записали mov al,0 ; старший байтик out 40h,al ; записали sti ; разрешаем прерывания Теперь компутерные часики тикают в 0FFh раз быстрее. Вот только есть несколько замечаний: 1. Компьютерные часы начинают сильно спешить. В принципе это не страшно. После получения Рандомного числа таймер перепрограмируется "как было" (старший байтик = 0FFh = младший байтик). Если за время работы поцедуры часы успеют убежать на пару секунд - это не страшно. А если страшно - тогда перед изменением таймера сохраняешь системное время mov ah,2ch int 21h и после завершения работы процедуры востанавливаешь системное время. 2. 40h порт - он по совместительству еще и работа с дисками (которые флопы). Поэтому с дискетками ты работать не сможешь (пока не перепрограмируешь обратно на 18.2). С винчестером все работает, а с флопиком нет. :-( 3. Я не проверял это под выньдовс. Возможно, работает это только в ДОСе. Ну а после этого изврата можно получать случайное число... Можно совместить эти два алгоритма. Т.е. после получения первого "случайного" числа ты "случайным" образом перепрограмируешь таймер, получаешь второе случайное число, снова случайно перепрограмируешь и т.д.. ----------------------------------------------------------------------- ВОПРОС: Как отформатировать дохлую дискету ? OLEGH: Во-первых, чтобы format не выяснял первоначальный формат дискеты,попробуй 'format a: /u'. Ключик /u означает что-то вроде unconditional format (безусловное форматирование) - у меня работало. Кстати, в DOS 6.22 этот ключик был документирован, а в DOS 7.10 из Win98SR2 я его не нашел... Но это не значит, что он не работает - нужно пробовать :) Если на нулевой дорожке есть физические повреждения - тогда это, конечно, не поможет. Наряду с другими способами можно попробовать воспользоваться программой для DOS под названием FF (Fast Format)by Shamarokov A. Чуть больше 70 К в архиве RAR. Она позволяет "восстанавливать" дискеты путем переразмещения секторов на дорожке (и обхода, таким образом, испорченных мест на поверхности). Она работает только под DOS. У меня есть, но похоже,не самая последняя версия и немного со странностями :). Если кто захочет ее попробовать, пишите, я вышлю отдельным письмом. Впрочем, если дорожка посыпалась, то ничего хорошего ждать не приходится ;(. Но еще эта прога умеет делать "быстрые" дискеты, т.е. с ускоренным доступом (используя специальное чередование секторов). Так что я пользуюсь только ей. ======================================================================= (С) HI-TECH group. All rights reserved and reversed. Оригинальная грамматика авторов сохранена.
http://subscribe.ru/
E-mail: ask@subscribe.ru | Отписаться | Рейтингуется SpyLog |
В избранное | ||