樱木灬晴子

樱木灬晴子 查看完整档案

北京编辑中国传媒大学  |  计算机 编辑  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

樱木灬晴子 发布了文章 · 2018-09-21

使用汇编语言编写加载器(加载用户程序)

使用汇编语言编写加载器加载指定格式的用户程序


在计算机加电之后,计算机首先会读取硬盘的主引导扇区,做一些必要的初始化工作,但是硬盘的一个扇区只有512字节,所以我们要实现更多的功能,就要有用户程序,我们需要把控制权限交给用户程序(操作系统暂且也算一种用户程序吧)。

在加载用户程序的过程中,主要分为以下几个大步骤:

  • 一.从硬盘读取用户程序,并加载到内存中的指定位置(自定义)。
  • 二.重定位用户程序(段地址)
  • 三.将控制权交给用户程序

用户程序头部:

SECTION header vstart=0                     ;定义用户程序头部段 
    program_length  dd program_end          ;程序总长度[0x00]
    
    ;用户程序入口点
    code_entry      dw start                ;偏移地址[0x04]
                    dd section.code_1.start ;段地址[0x06] 
    
    realloc_tbl_len dw (header_end-code_1_segment)/4
                                            ;段重定位表项个数[0x0a]
    
    ;段重定位表           
    code_1_segment  dd section.code_1.start ;[0x0c]
    code_2_segment  dd section.code_2.start ;[0x10]
    data_1_segment  dd section.data_1.start ;[0x14]
    data_2_segment  dd section.data_2.start ;[0x18]
    stack_segment   dd section.stack.start  ;[0x1c]
    
    header_end:  

一.读取用户程序到内存

从硬盘读取信息,需要五个步骤:

  1. 设置要读取的扇区数量

这个数值要写入0x1f2端口,这是一个8位寄存器,所以可以使用

out dx,al

2.设置要读取的起始LBA扇区号。
这里使用LBA28。28位的扇区号,分给四个端口,0x1f3-0x1f6,从低到高依次存储,最后一个端口也就是0x1f6低四位存储扇区号的最高四位,剩下四位高三位111表示LBA模式,最低以为0表示从盘,1表示主盘。
例如,如果其实扇区是100,也就是0x60,那么设置的代码如下:

mov dx,0x1f3
mov al,0x60
out dx,al
inc dx
xor al,al
out dx,al
inc dx
out dx,al
inc dx
mov al,0xe0
out dx,al

3.请求读写
次数值写入0x1f7端口,0x20表示请求读

mov al,0x20
mov dx,0x1f7
out dx,al

4.等待硬盘空闲
0x1f7这个端口,除了可以请求读意外,还能表示硬盘的状态,第7位0表示空闲,1表示繁忙,第3位为1表示准备好进行数据传输,所以这里我们要等待硬盘空闲才可以传输数据:

waits:
    in al,dx
    and al,1000_1000B    ;保留第3位和第7位
    cmp 0000_1000B    ;第7位为0第3位为1才可以进行读取
    jne waits        ;不相等就循环等待

5.读数据
从硬盘读取数据,通过0x1f0端口,这是一个16位寄存器。硬盘是典型的块设备,所以一次必须读取512字节,或者它的倍数。比如,我们要读取一个扇区的数据,并存放在ds:0开始的内存空间:

    mov dx,0x1f0
    mov cx,256    ;读取一个扇区,512字节,即256字
    xor bx,bx
read_word
    in ax,dx
    mov [bx],ax
    add bx,2
    loop read_word

6.检查用户程序是否读取完整。
刚才我们只读取了一个扇区,即512字节,我们并不能确定用户程序是否已经完全读完,但是我们已经读取了用户程序的头部,这里我们规定用户程序头部的第一个双字必须定义用户程序的长度.所以我们从ds:0的位置读取两个字,第一个字放在ax,第二个字放在dx,这样dx:ax代表了用户程序的总长度,用这个数除以512得到商和余数,可以知道用户程序的读取进度,进而来决定是继续读取还是跳转到后边的步骤(重定位)。
注意:
由于一个逻辑段最大是64kb,从0x0000-0xffff,但是用户程序可能超过这个范围,为了避免这种事情发生,我们每读一个扇区,便把段地址加0x20(512),这样便可以连续存放且不用担心超过逻辑段大小。

    ;检查用户程序是否读取完整
    xor bx,bx
    mov ax,[bx]
    mov dx,[bx+2]
    mov bx,512
    div bx
    
    cmp dx,0
    jne cmp_ax
    dec ax
    
cmp_ax:
    cmp ax,0
    je  redirect_entry    ;跳转到重定位
;读取剩余扇区
    push ds
    
    mov cx,ax
    mov si,start_sector    ;start_sector是定义的一个常数,这里等于100(用户程序在100扇区开始)
    
read_rest:
    mov ax,ds
    add ax,0x20
    mov ds,ax
    inc si
    call read_disk
    loop read_rest
    
    pop ds

二.重定位用户程序

用户程序在编写的时候都是分段的,重定位的目的便是确定每个段的实际段地址。
这里我们规定用户程序头部中定义了每个段的段首位置(汇编地址),转换成16位的段地址并重新写入。

;重定位用户程序
;重定位用户入口点的段地址
redirect_entry:
    mov ax,[0x06]    ;读取头部入口点地址信息
    mov dx,[0x08]
    call calc_seg_base
    mov [0x06],ax
    
;重定位其他段的段地址
    mov cx,[0x0a]
    mov bx,0x0c
redirect_other_seg:
    mov ax,[bx]
    mov dx,[bx+2]
    call calc_seg_base
    mov [bx],ax
    add bx,4
    loop redirect_other_seg
;过程计算段地址
;已知dx:ax物理地址,求出段地址并存放在ax中返回    
calc_seg_base:
    push dx
    
    add ax,[cs:usr_app_base]
    adc dx,[cs:usr_app_base+2]
    shr ax,4
    ror dx,4
    and dx,0xf000
    or  ax,dx
    
    pop dx
    
    ret

三.将控制权交给用户程序

通过jmp far 命令进行段间跳转,这里是,跳转到重定位后的入口点位置,存放在ds:0x04处

jmp far [0x04]

至此,我们的加载器就基本完成了,可以用它来加载任何符合我们规定格式的用户程序(用户程序头部)。

查看原文

赞 0 收藏 0 评论 0

樱木灬晴子 关注了标签 · 2018-06-01

关注 194

樱木灬晴子 关注了用户 · 2018-01-02

vczh @vczh

长期开发跨三大PC平台的GUI库 http://www.gaclib.net,讨论QQ群:231200072

关注 2369

樱木灬晴子 发布了文章 · 2017-11-22

你不知道的C语言--第一篇——编译和运行

相信很多理工科的同学在大学期间都或多或少的学过C语言,但由于老师教的不用心或者当时我们知识的限制,导致我们对C语言存在许多误解。并且我们在学习c语言时,为了方便省事,大多数人都使用IDE,导致我们对编译的过程完全没有概念。
博主现在是一名大三的本科生,有幸读到宋劲杉老师的《一站式学习C编程》,发现了许多原来对于c语言的错误和模糊的认识,所以总结出来供自己复习并与大家分享
有错误的地方请大家及时指正,谢谢!

编译执行

对于我们初学编程时都要写的hello world程序,相信大家都非常熟悉了,但是即使是这样一个简单的程序,我们也未必都全能掌握,下面是helloworld代码:

#include <stdio.h>
int main(void)
{
        printf("Hello, world.\n");
        return 0;
}

要在Linux上不借助任何IDE编译运行它,我们需要在命令行输入一系列的命令:

$ gcc main.c -o main
$ ./main
Hello, world.

其中,gcc是linux下c语言的编译器,gcc main.c -o main的意思是编译main.c生成可执行文件main,这个main可以自己命名。

对于编译时的警告,有时候并不会全部提醒,但如果我们在编译时加上-Wall参数,编译器便会显示出所有的警告,供我们参考修改程序:gcc -Wall main.c -o main


编译时和运行时

大家在初学c语言时,一定会被编译时和运行时搞得一头雾水,因为那时候我们使用IDE一键运行,对于我们来说编译运行似乎就是一样的。然而有了上面的知识,我们就很好区分编译时和运行时了。

所谓编译时,就是我们在命令行输入gcc编译文件后产生可执行文件的过程,在这段时间出现的错误,就是编译时错误,一旦编译时出现了错误,就不会再产生可执行文件了,编译就被中断了。

而运行时错误就是在编译通过,生成可执行文件后,我们运行这个可执行文件之后产生的错误,这种错误一般更难捕捉。

如我们在学习全局变量和局部变量时有一条规定:

局部变量可以用类型相符的任意表达式来初始化,而全局变量只能用常量表达式来初始化。

之所以会有这样一条规定,就是因为全局变量必须在程序开始运行时初始化完成(运行任何语句之前),所以初始值必须保证保存在编译生成的可执行文件中,因此初始值必须在编译时计算出来。所以为了简化编译器,才规定全局变量只能用常量表达式来初始化。

问题的关键还是要区分编译时运行时

参考文献: 《一站式学习C编程》 宋劲杉

查看原文

赞 1 收藏 0 评论 6

樱木灬晴子 回答了问题 · 2017-08-04

解决ruby重命名文件权限报错

好了,已经解决了,原来忘记把"."和".."去掉了,去掉之后发现路径名是中文的话会有编码问题,为了省事我直接把路径名换成英文了,然后就可以了,新的代码如下:

Dir.foreach ("BBC.Life") do |filename|
    if newfile=filename.sub(/\[.*\]\./,"")
        #File.chmod(0755, filename) rescue nil
        next if filename=="."
        next if filename==".."
        File.rename("BBC.Life/"+filename,"BBC.Life/"+newfile)
        #print filename,"   =>  ",newfile,"\n"
    end
end

关注 1 回答 1

樱木灬晴子 提出了问题 · 2017-08-03

解决ruby重命名文件权限报错

今天打算批量重命名一个目录下的所有文件
代码如下:

Dir.foreach ("生命") do |filename|
    if newfile=filename.sub(/\[.*\]\./,"")
        #File.chmod(0755, filename) rescue nil
        File.rename(filename,newfile)
        #puts File.writable?(filename)
    end
end

结果报错:Permission denied,截图如下

图片描述

大家有遇到过这种情况吗,求大神解决!!

关注 1 回答 1

樱木灬晴子 提出了问题 · 2017-07-06

解决Rails向数据库添加新字段没添加上

刚接触Rails,按照headfirst上的教程,向一个已经创建好的数据库中添加新的字段phone,但是好像没有添加进去,代码如下:

rails generate migration AddPhoneToTickets phone:string

并使用迁移:

rake db:migrate

项目下的db下的migrate下的文件也变了:

class AddPhoneToTickets < ActiveRecord::Migration[5.1]
  def change
    add_column :tickets, :phone, :string
  end
end

但是在网页上提交的时候还是没有添加进去,如图:
图片描述

求大佬解答!!多谢

关注 2 回答 1

樱木灬晴子 回答了问题 · 2017-05-08

求SQL语句

合计一合计二和合计三是三个列的列名吗,你是想分别计算这三个列的和?
如果是这个意思的话,那试试
select sum(合计一) from 表名;
这样计算出合计一列的和,其他两个一样

关注 3 回答 2

樱木灬晴子 提出了问题 · 2017-05-07

解决关于c++对象数组初始化时产生的临时变量析构问题?

第一种情况:

创建c++对象数组时候,如果通过初始化列表赋值,会产生临时变量,但为什么不会调用析构函数
代码如下:

#include <iostream>
    using namespace std;
    class Point
    { public:
        Point():x(0),y(0)
        {   cout<<"Default Constructor called.\n";}
        Point(int xx,int yy):x(xx),y(yy)
        {   cout<< "Constructor called.\n";  }
        ~Point()
        {   cout<<"Destructor called.\n";    }
        void Move(int xx,int yy)    {  x=xx;  y=yy;   }
    private:
        int  x,y;
    };
    int main()
    {
        Point A[2]={Point(1,2),Point()};
        cout<<"hello"<<endl;
        return 0;
    }

输出结果为:

图片描述


其中,Point A[2]={Point(1,2),Point()};通过两个临时变量初始化对象数组A,之后为什么不会调用析构函数将其析构,结果中并未调用这两个临时对象的析构函数。

第二种情况:
分别为数组的每个元素显示调用构造函数,就会自动调用析构函数:
代码如下:

#include <iostream>
using namespace std;
class B
{
    int x, y;
public:
    B();
    B(int i);
    B(int i, int j);
    ~B();
    void Print();
};
B::B() :x(0), y(0)
{
    cout << "Default constructor called.\n";
}
B::B(int i) : x(i), y(0)
{
    cout << "Constructor 1 called.\n";
}
B::B(int i, int j) : x(i), y(j)
{
    cout << "Constructor 2 called.\n";
}
B::~B()
{
    cout << "Destructor called.\n";
}
void B::Print()
{
    cout << "x=" << x << ", y=" << y << endl;
}
int  main()
{
    B *p;
    p = new B[3];
    cout << "*******" << endl;
    p[0] = B();
    p[1] = B(7);
    p[2] = B(5, 9);
    for (int i = 0; i < 3; i++)
        p[i].Print();
    cout << "*******" << endl;
    delete[]p;
}



运行结果如下:
图片描述


可见在为每个数组元素调用构造函数初始化后,临时变量析构了。


综上两种情况都是临时变量,但是为什么一个会自动析构,另一个不会,求解!!!

关注 4 回答 3

樱木灬晴子 回答了问题 · 2017-05-07

解决win764位环境下,我用GCC为什么指针占8个字节,而long是4个字节?

我记得深入理解计算机系统上说,指针占计算机的全字长,64位环境字长是64所以指针是64位,8个字节,至于long是具体的编译环境决定吧

关注 3 回答 2

认证与成就

  • 获得 2 次点赞
  • 获得 11 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-07-27
个人主页被 512 人浏览